Featured image of post 帶著Java記憶轉生為Golang工程師

帶著Java記憶轉生為Golang工程師

上輩子我是一個內卷過度的Java碼農,這次轉世重來,我帶著前世的記憶轉生為Golang碼農...

Golang的Main方法

1
2
3
4
5
6
package main

func main() {
	print("Hello, World!")
}

Java與Golang的不同-宣告變數

這應該是大家最快認為Java跟Golang不同的地方,在Java中,撇除掉Java10加入的 var 宣告,Java的變數宣告格式大略如下,也沒有其他方式宣告了

1
Integer age = 10;

在Golang中則是有三種不同的方式去宣告一個變數

  1. 標準變量聲明,這個最類似Java,與Java不同在於前面加了一個var,並且是先聲明變量名稱再宣告型別
1
var age int = 10
  1. 類型推斷宣告,有點類似javascript的宣告,讓編譯器自己去推斷age的型別為int
1
var age =10
  1. 短變量聲明(簡短賦值),golang常見的簡便聲明方式,由於var這個詞太頻繁出現了,就用:=來取代它
1
age :=10

短變量聲明(:=)只允許在函式內部使用,如果是package Level的則不行

var還有一種Java沒有的用法,就是「宣告串列」可以用這種方式一次宣告多個變數

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
	var (
		age    = 10
		name   = "John"
		salary string
	)
	print(age)
	print(name)
	print(salary)
}

Java與Golang的不同-未使用的區域變數會導致編譯錯誤

在Golang中,為了程式碼的精簡,是「不允許有未使用的變數」存在於程式碼之中的,比如說在Java中,我們可能有時候在函式內部聲明了一個orderId,但久了未使用就繼續讓它存在下去。

1
2
3
4
5
public void saveItem(){
...
String orderId = orderDao.getOrderId();
return void;
}

但在Golang中,是不允許這樣的行為的,編譯器跟IDE都會跟你說「Unused variable ‘orderId’」

image-20240915142615257

但未被使用的常數是允許的

1
2
3
4
5
6
const sum = 20 * 10

func main() {
	print("Hello")
}

Golang的基本型態

  • 布林:主要就是True跟False,在Golang裡面這樣宣告,跟Java差不多
1
2
3
4
func main() {
	var isOK bool = true
	print(isOK)
}
  • 字符串:主要用法也跟Java差不多
1
2
3
4
5
func main() {
	var name string = "Hoxton"
	print(name)
}

  • 整數:跟Java只有一個int不同,Golang的整數包含了int, int8, int16, int32, int 64, uint, unit8, uint16, uint32, uint 64,int開頭的就代表是signed(帶正負號),而uint就代表unsigned(未帶正負號的)。int8就代表了 -128~127,uint8就代表0-255,以此類推。

    通常就是先使用 int 就可以了,在不同系統上的 int 代表的範圍不一樣,如果是32位元的int範圍就是-231~+231,如果是64位元的系統則是-263~+263

1
2
3
4
func main(){
  var age int = 27
  print(age)
}
  • 浮點:跟Java只有一個float的不一樣,golang分成float 32 以及 float64 ,這邊除非特殊原因也是無腦選擇使用float64,就可以了。關於精度的問題當然也跟所有的程式語言一樣,會有精度丟失的問題,進得不要使用它來表達金額
1
2
3
4
5
func main() {
	var size float64 = 5.0
	print(size)
}

  • 複數:不可能吧,你不可能想在golang上使用複數吧,由於golang本身不支援矩陣,以及種種的一些原因,如果你想在golang裡面進行這些運算,可以考慮使用Gonum這個程式包,但在Golang上進行複數運算是個不太適合的行為,已經有人在討論會在未來的版本移除Golang對複數的支援了

Java與Golang的不同-不允許自動型態轉換

在Java中,我們是允許不同型別的數字相加的,比如說

1
2
3
4
5
6
   public static void main(String[] args) {
        float height =10.0f;
        int width = 20;
        System.out.println(height+width);
        //印出 30.0
    }

但在Golang中,這種事情是不允許的,Golang不允許變數的自動型態提升

1
2
3
4
5
func main() {
	var height int = 10
	var weight float64 = 20.5
	print(height + weight)
}

甚至編譯器本身就會報錯,提醒你

「Invalid operation: height + weight (mismatched types int and float64)」

image-20240915141300826

Java與Golang的不同-宣告一個final變數使用的是const關鍵字

1
2
3
4
func main() {
	const greeting = "hello"
	print(greeting)
}

有個跟Java比較不一樣的是,在Java中如果我們宣告一個為final的常數,我們通常會用All UPCASE的方式命名,如

1
2
private final INTEGER AGE_LIMIT=100
private final BIGDICEL TAX_RATE=0.5

但在golang中,使用package level等級的宣告名稱的第一個字母的大小寫,會被用來確認這個常數能不能被程式包外面的程式使用,也就是說,首字母的大小寫與否會控制常數的private of public,因此如果你的常數不是一個global的常數,請不要用大寫開頭來命名

複合型別-Array、Map、Struct

Array

宣告方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
    //1. 第一種宣告,宣告陣列長度,不宣告陣列內部元素
    var array [3]int
    //2. 第二種宣告,宣告陣列長度,宣告陣列內部元素
    var array2 = [3]int{1, 2, 3}
    //3. 第三種宣告,不宣告陣列長度,宣告陣列內部元素
    var array3 = [...]int{1, 2, 3, 4, 5}

    print(array[0])
    print(array2[0])
    print(array3[0])

}

Go的Array相較於Java來說非常不好用,一個最重要的概念是,Go的陣列大小是陣列型態的一部分,意味著長度為4的陣列和長度為5的陣列,雖然看起來都是陣列,但它們其實有不一樣的型態,以Java來說,我們可以寫一個方法,打印出所有的字串陣列元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        String [] arr = {"a", "b", "c"};
      	String [] arr2 ={"d", "e", "f"};
        printArray(arr);
      	printArray(arr2);
    }

    static public void printArray(String[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

但在Golang中,我們是沒辦法這樣子的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
	//1. 第一種宣告,宣告陣列長度,不宣告陣列內部元素
	var array [3]int
	//2. 第二種宣告,宣告陣列長度,宣告陣列內部元素
	var array2 = [3]int{1, 2, 3}
	//3. 第三種宣告,不宣告陣列長度,宣告陣列內部元素
	var array3 = [...]int{1, 2, 3, 4, 5}

	print(array[0])
	print(array2[0])
	print(array3[0])
	printArray(array)
	printArray(array2)
	printArray(array3)
}
func printArray(arr []int) {
	for _, value := range arr {
		fmt.Print(value, " ")
	}
	fmt.Println()
}

「Cannot use ‘array3’ (type [5] int) as the type [] int」

image-20240915154606868

如果你要使用的話,可以將printArray吃的參數變為陣列長度為3的參數,但這樣,長度為5的array3還是不能使用

image-20240915154823100

更好的Array-Slice

既然Array這麼難用,那我們有時候會需要Array的性質做一些處理時該怎麼做呢?可以使用Slice,Slice是Golang最實用的功能之一,比較一下Slice跟Array兩者的差別

  1. Slice的宣告不需要指定大小
1
2
3
4
5
6
7
func main() {
	var array = [3]int{1, 2, 3}
	var slice = []int{4, 5, 6}
	
	fmt.Println(array, slice)
	
}
  1. Slice未賦值時會是nil (一種像Java null但又不全然是null的東西,代表的是某些型態缺值)
1
var slice []int
  1. Slice不能拿來跟nil以外的東西做==, !=的比較,會出現編譯錯誤,只能跟nil做比較
1
2
3
4
5
6
func main() {
	var slice1 = []int{4, 5, 6}
	var slice2 = []int{4, 5, 6}
	print(slice1 == slice2)

}

image-20240915155528731

  1. slice有容量的概念,所謂容量,相較於slice的長度,就類似Java中ArrayList的自動擴容一樣。當我們宣告一個slice時,其實是在記憶體中選取一塊連續的空間來寫入這些值,而容量就是這個連續空間的大小。當你的容量等於slice長度時,golang就會去自動配置更大的容量來適配它

Slice的用法

  • len:取得長度
1
2
3
4
func main() {
	var slice1 = []int{4, 5, 6}
	print(len(slice1))
}
  • cap:取得目前slice的連續記憶體容量,很少用,通常是用來檢查slice是否大到難以容下新資料

  • append:加值進去slice,「一定會增加slice的長度」,並且會把值塞到最後一位,有可能導致前面的值全部都為0

1
2
3
4
5
6
7
8
9
func main() {
	var slice1 = []int{4, 5, 6}
	println(len(slice1))
	//長度為3
	slice1 = append(slice1, 7)
	println(len(slice1))
	//長度為4
}

  • make:宣告slice的長度,宣告slice的容量
1
2
3
4
5
6
7
8
9
func main() {
  // 僅宣告長度,不宣告容量
	var slice1 = make([]int, 5)
	println(len(slice1))
	//長度為5
  
  // 長度為5,容量為10
  var slice2 = make([]int, 5, 10)
}

如何很Go的宣告一個slice

核心目標就是減少slice的擴增次數,如果slice完全不需要擴增,那就使用var並且不設值來建立nil slice

1
var data [] int

又或者是說你的slice有一些初始值,或是slice的值不會變,那麼slice常值是個很好的選擇

1
data := []int{2,4,6,8}

還有一種情況比較複雜,就是你「大概」知道slice有多大,但不確定具體的值是多少,比如說你可能一個商品可能有不同顏色,你知道最少有一個,最多不超過十二個,那你要將slice宣布成多少呢?

Well,這有三種流派的做法

  1. 如果將slice當成一個緩衝區來用,那就指定一個非零長度
1
2
3
4
5
func main() {
	var itemColor = make([]string, 6)
	println(len(itemColor))
	println(cap(itemColor))
}
  1. 如果「精確」的知道想要的大小,那可以指定長度,並檢索slice來設定值,但這問題的缺點在於,如果你指定了錯誤的大小,要嘛slice的結尾有許多零值,要嘛slice會存取不存在的元素觸發panic
  2. 最推薦的作法則是在make時,長度設定為0,並指定容量,如此一來可以使用append來將item加入slice中,這種做法比起前兩種會稍微慢一點,但它造成Bug的機率最低
1
2
3
4
5
func main() {
	var itemColor = make([]string, 0,12)
	println(len(itemColor))
	println(cap(itemColor))
}

Slice的切割

slice顧名思義就是切割,用法有點像python,就像這樣

1
2
3
4
5
6
7
func main() {
	slice := []int{1, 2, 3, 4, 5, 6, 7}
	//: 的左邊右邊不寫,就代表是第一位跟最後一位
	fmt.Println(slice[1:])
	fmt.Println(slice[:1])
	fmt.Println(slice[:])
}

注意, 從slice取slice出來,不會製造副本,而是類似Java的淺拷貝一樣,除此之外,子 slice 還有一些特性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
    father := []int{1, 2, 3, 4, 5}
    child := father[:3]
    fmt.Println("father:", father)
    fmt.Println("child:", child)

    child = append(child, 30)

    fmt.Println("father:", father)
    fmt.Println("child:", child)

}

執行完這段程式碼,你會發現一些令人費解的事情

father: [1 2 3 4 5] child: [1 2 3] father: [1 2 3 30 5] child: [1 2 3 30]

這其中的原因有點複雜,大意大概是,當你從一個slice中取出另一個slice時,子slice的容量是原始slice的容量減去子slice在原始slice內的位移(offset)。也就是說,原始slice未使用的容量也會與任何子slice共享。

slice的注意事項

總之,為了避免產生一些奇怪的現象,絕對不要同時使用append與子slice,要再次強調,父子slice會共用同一段記憶體,而且修改其中一個,都會影響另一個,不要在切割slice後修改它們!!!

slice的複製

如果想要擁有獨立的slice,可以使用copy函數

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
	father := []int{1, 2, 3, 4, 5}
	child := make([]int, 5)
	num := copy(child, father)
	fmt.Println(num)
	fmt.Println("father:", father)
	fmt.Println("child:", child)

	child = append(child, 30)

	fmt.Println("father:", father)
	fmt.Println("child:", child)

}

Map

Golang Map的宣告跟Java不太相同

1
HashMap<String, Integer> map = new HashMap<String, Integer>();

而Go則是

1
2
3
var stringMap  = map[string]int{}
stringMap := map[string]int{}
stringMap := map[string]int{"one": 1, "two": 2, "three": 3}

而Map跟Slice也有一些特性是共通的

  1. Map也會自動擴充
  2. 可以使用make來指定Map的長度
  3. 也可以調用len得知長度
  4. Map的零值是nil
  5. Map無法被比較,只能用來檢查是否等於nil

Golang的Map與Java的Map用法大置雷同,但還是有一些差別,以下說明差異的部分

Java與Golang的不同-逗號ok(comma ok)的寫法

簡單來講就是用來驗證map中是否存在某個Key值,如下

1
2
3
4
5
6
7
func main() {
	studentMap := map[int]string{1: "Hoxton", 2: "Ian", 3: "Jenny", 4: "Jenny", 5: "Jenny"}
	v, ok := studentMap[1]
	fmt.Println(v, ok)
	v2, ok2 := studentMap[6]
	fmt.Println(v2, ok2)
}

打印的結果

1
2
Hoxton true
 false

這種逗號ok的寫法,會將map的結果指派給兩個變數,第一個會是索引的值,第二個則是bool,名稱通常為ok,代表索引鍵是否存在於map中

Java與Golang的不同-Golang中不存在set,使用Map來模擬操作

大意就是用Map不能重複的性質來去做類似Set的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	orderIdMap := map[int]bool{}
	orderIdSlice := []int{1, 2, 3, 3, 3, 3}
	for _, orderId := range orderIdSlice { // 遍历切片
		if _, ok := orderIdMap[orderId]; !ok { // 如果map中不存在该orderId
			orderIdMap[orderId] = true // 添加到map中
		}

	}
	fmt.Println(orderIdMap)

}

Struct

Struct跟Class有什麼差別呢?很簡單!Struct沒有繼承的概念!x

Go宣告一個struct,以及賦值的方式如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
	type person struct {
		Name string
		age  int
		Pet  string
	}

	hoxton := person{Name: "hoxton", age: 22, pet: "dog"}
	fmt.Println(hoxton)
	ian := person{Name: "ian", age: 22}
	fmt.Println(ian)

}

Java有匿名Class,那麽golang當然也是不會錯過的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {

	pet := struct {
		Name string
		Type string
	}{Name: "red", Type: "dog"}

	fmt.Println(pet.Type)

}

如果有兩個struct,他們的內部的Field名稱、Field數量、Field順序、Field型別等等,全部都相同,唯一不同的只有struct名稱不一樣,那麼這兩個struct是可以相互轉換的

Go的控制結構-If, For

If

Go的If還蠻正常的,如下,跟其他語言的差別在於條件的部分沒有加上小括號而已

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	number := rand.Intn(10)
	if number == 0 {
		fmt.Println(number, " number Is 0")
	} else if number > 5 {
		fmt.Println(number, "number is greater than 5")
	} else {
		fmt.Println(number, "number is less than 5 , but not 0")
	}

}

另外

Licensed under CC BY-NC-SA 4.0