# Go ### 指派 ### new 函式 new 是一個內建的函式,用來建構變數,`new(T)` 會申請一塊記憶體,以 T 的零值初始化,並**回傳指標 \*T** ```go= p := new(int) fmt.Println(*p) // 0 *p = 2 fmt.Println(*p) // 2 ``` new 是內建的函式,不是關鍵字,所以能在函式中重新宣告 ```go= func delta(old, new int) int { reuturn new - old } ``` ### new 和 make 的區別 make 用來初始化內建的複合資料型態 slice、map、channel,`make(T)` **回傳的是 T 而不是 \*T** ### 變數生命週期 變數存活直到他不可再被觸及為止 編譯器可以選擇在 stack 或 heap 上分配變數,並非由使用 new 或 var 決定 ```go= var global *int func f() { var x int x = 1 global = &x } func g() { y := new(int) *y = 1 } ``` x 分配在 heap,y 分配在 stack,注意全域變數保存不必要的短生命週期物件指標會阻止 gc 回收記憶體,在效能最佳化時請考慮這一點 ### 資料組指派 一次指派多個變數,**右手邊的全部運算式會先求值,然後才更新變數** ```go= func gcd(x, y int) int { for y != 0 { x, y = y, x%y } return x } ``` ### 型別宣告 int 可以用來表示迴圈的索引、日期的月份 string 可以表示密碼、顏色名稱... 用 type 宣告定義一個和底層型別相同的新具名型別,目的是分離底層的型別,避免搞混 Fahrenheit、Celsius 二種溫度單位,底層都是 float64,但彼此不能直接比較、做運算,分離型別可以避免錯誤地使用 ```go= type Celsius float64 type Fahrenheit float64 const ( FreezingC Celsius = 0 CelsiusC Celsius = 100 ) func CtoF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } func FtoC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) } ``` 1、2 一邊是具名型別 (Celsius/Fahrenheit),另一邊是具名型別的底層型別時(float64),比較運算子可以使用 4 是型別轉換,二個底層都是 float64 的情況下,不會改變值的表示,二個都是 0 所以是 true 型別轉換不會在 runtime 失敗,但要注意字串、slice、float、有號數字/無號數字轉換時可能改變值的表示 ```go= var c Celsius var f Fahrenheit fmt.Println(c == 0) // true fmt.Println(f >= 0) // true fmt.Println(c == f) // 編譯錯誤 fmt.Println(c == Celsius(f)) // true ``` 當底層型別是 `float64` 簡單的型別時差異不大,但若是複雜型別,例如 struct,新具名型別能夠避免反覆寫複雜的型別轉換 ### 範圍 `if`、`switch` 除了本體區塊外也建構隱藏區塊 ```go= if x := f(); x == 0 { fmt.Println(x) } else if y := g(x); x == y { fmt.Println(x, y) } fmt.Println(x, y) // 編譯錯誤 ``` 第二個 if statement 套疊在第一個內,因此在第一個 if 宣告的變數在第二個 if 中可見 使用短變數宣告務必注意範圍 ```go= var cwd string // 全域的 cwd 並未被正確初始化 func init() { cwd, err := os.Getwd() if err != nil { log.Println(err) } log.Println(cwd) } ``` 解決方式是使用獨立的 var 宣告 ```go= var cwd string func init() { var err error cwd, err = os.Getwd() if err != nil { log.Println(err) } log.Println(cwd) } ``` ## 3. 基本資料型別 Go 的資料型別有 1. 基本型別:數字、字串、布林 2. 集合型別(aggregate):陣列、struct 3. 參考型別:slice、map 4. 介面型別 ### 整數 使用 `int`、`uint` 時編譯器會選擇大小最有效率的整數(通常是 32/64),但在相同硬體上不同編譯器可能做出不同選擇 rune 是 int32 的同義詞,用來表示該值為 unicode byte 是 uint8 的同義詞,用來表示該值為原始資料 uintptr 用於低階程式設計,足夠保留指標值的所有位元 #### 數學運算子 % 餘數運算只能用於整數,負數在各程式語言有不同實作定義,Go 將餘數的正負號取決於被除數的正負號 ```go= -5%-3 = -2 -5%3 = -2 ``` #### 無號數字陷阱 雖然 go 提供了無號數的型別與運算,但即使不會是負值的情況,個人仍傾向使用 int 內建的 `len()` 也是回傳 int,我認為這可以避免隱晦的錯誤,例如 ```go= nums := []string{"one", "two", "three"} for i := len(nums)-1; i >= 0; i-- { fmt.Println(nums[i]) } ``` `len()` 若回傳無號數,則 `i` 也會是 uint,而 `i >= 0` 永遠為真,`i == 0` 時,`i--` 不會變成 -1,而是 uint 最大值(可能是 $2^{64}-1$),這段程式會嘗試存取界外元素造成 runtime error 所以無號數資料型別只用於位元運算、某些特定數學運算子有要求(雜湊加密、實作位元組、解析二進制檔案...),否則即使非負值仍應該使用有號數資料型別 p.s. 使用 `int`、`uint` 位元數為實作定義,編譯器會選擇大小最有效率的整數(通常是 32/64),但在相同硬體上不同編譯器也可能做出不同選擇 fmt 使用的小技巧,`Printf()` 格式字串用到多個 % 修飾符時,可以使用 [1] 重複第一個運算元 ```go= x := int64(0xdeadbeef) fmt.Printf("%d %[1]x %#[1]x %#[1]X", x) ``` ### 浮點數 Go 有 float32、float64 二種,受 IEEE754 規範 float32 精度為 6 位數,float64 精度為 15 位,大部分情況使用 float64,因為 32 的計算重複很快會累積誤差,且能夠精確表示的最小正整數不夠大 ```go= var f float32 = 1 << 24 // 16777216 fmt.Println(f == f+1) // true ``` 很大或很小的浮點數最好使用科學記號法寫出 浮點數 printf - %g 自動選擇適合的精度 - %f 預設 6 位 - %e 指數 `math.IsNaN()` 測試是否為非數字的值,而 `math.NaN()` 回傳一個 NaN (表示非數字的值),但要注意 NaN 不能互相比較,因為 NaN 相比永遠為 false ```go= nan := math.NaN() fmt.Println(nan == nan) // false ``` ### 布林值 && 的優先比 || 高 布林值與 1, 0 數值不會間接轉換 ```go= i := 0 if b { i = 1 } ``` 如果需要轉換操作,可以寫函式 ```go= func btoi(b bool) int { if b { return 1 } return 0 } func itob(i int) bool { return i != 0 } ``` ### 字串 字串是不可變的一系列 byte,文字字串通常解釋成 `rune` (Unicode) 內建的 `len()` 回傳的是 byte 數量,而非 rune 數量(rune 可能 1~4 個 byte) 透過索引操作 `s[i]` 時,是存取第 i 個 byte,第 i 個 byte 不一定會是第 i 個 rune(非 ASCII 需要二個或以上的 byte) ```go= var s string = "λ" // non ASCII var r []rune = []rune(s) fmt.Println(len(s)) // 2 fmt.Println(len(r)) // 1 fmt.Println(cap(r)) // 32 fmt.Println(string(s[0])) // Î ``` 使用 `==` 、 `<` ... 運算子比較時,是逐個 byte 進行,所以結果會是字典序 ### UTF-8 UTF-8 是方便的交換格式,但在程式中使用 `rune` 更方便,因為同樣大小會方便在陣列和 slice 中索引 UTF-8 編碼的 rune 使用介於 1~4 個 byte (len() 可能 1 ~ 4) 表示(但 cap() 都是 32),第一個 byte 的 MSB 表示後面有幾個位元組 ``` 0xxxxxxx 110xxxxx 10xxxxxx 1110xxxx 10xxxxxx 10xxxxxx 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ``` 變化的長度也導致無法透過索引直接存取第 n 個字元 UTF-8 的特質使得許多字串操作不需要解碼 ```go= // 檢查前綴 func HasPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix } // 檢查後綴 func HasSuffix(s, suffix string) bool { return len(s) >= len(suffix) && s[len(s) - len(suffix):] == suffix } // 子字串 func Contains(s, substr string) bool { for i := 0; i < len(s); i++ { if HasPrefix(s[i:], substr) { return true } } return false } ``` rune 轉換 string ```go= s := "ABC" r := []rune(s) fmt.Println(string(r)) // ABC ``` ```go= s := "abc" b := []byte(s) s2 := string(b) ``` string 轉換 `[]byte` 和 `[]byte` 轉換 string 都會複製新的 byte 陣列,以避免被互相改動,只有某些最佳化的編譯器在特定情況下可以避免複製新的 byte 陣列 對 string 做大量修改操作時,為了避免不斷複製(string 不可變),建議改用 `bytes.Buffer` 這個型別,不要用 string ```go= var b bytes.Buffer b.Write([]byte("Hello")) b.WriteTo(os.Stdout) // Hello fmt.Fprintf(&b, "World") b.WriteTo(os.Stdout) // World ``` ### 常數 常數是編譯器已知值的運算式,求值保證發生於編譯期;底層是基本型別:布林、字串、數字 一次宣告多個常數時,省略會使用前一個的運算式和型別 ```go= const ( a = 1 b c = 2 d ) // 1 1 2 2 ``` iota 常數產生器,建構一系列相關值而無須明確的指定 ```go= const ( _ = 1 << (10 * iota) KiB // 1024 MiB // 1048576 GiB TiB PiB EiB ZiB YiB ) ``` ### 無型別常數 常數可以是任何資料型別,例如 int、time.Duration 具名型別,但如果沒有指定,就不屬於特定型別,這種常數有 1. 無型別布林 2. 無型別整數 3. 無型別浮點數 4. 無型別複數 5. 無型別字串 6. 無型別 rune 有二個好處,一是編譯器將這些不屬於特定型別的常數用比基本型別更高的精度表示,可以假設至少有 256 位元的精度 YiB、ZiB 的值對整數來說太大 ```go= fmt.Println(YiB/ZiB) // 1024 ``` 二是可以省去轉型,像是 math.Pi ```go= var x float32 = math.Pi var y float64 = math.Pi // 不需要轉型 const Pi64 float64 = math.Pi var z float32 = float32(Pi64) ``` 只有常數可以無型別,將常數指派給變數時,會間接轉換型別 ```go= var f float64 = 3 + 0i // untyped complex -> float64 f = 2 // untyped integer -> float64 f = 1e123 // untyped floating -> float64 f = 'a' // untyped rune -> float64 ``` 但要注意,常數從一個型別轉成另一個型別時,目標要能表示原始值(浮點數複數、實數可以捨去) ```go= const ( ) ``` 如果沒指定,會透過常數間接決定該變數的預設值 ```go= i := 0 // int(0) r := '\000' // rune('\000') f := 0.0 // foat64(0.0) c := 0i // complex128(0i) // 明確轉型 var i = int8(0) var i int8 = 0 ``` @todo 聽說這個預設值,在無型別常數轉換介面值時很重要(? --- ## 4. 組合型別 ### 陣列 因為是固定長度,所以 Go 的陣列比較少用,用 slice 比較彈性 陣列大小是型別的一部分,所以 `[1]int` 和 `[2]int` 是不同型別 陣列初始化為該型別的零值 ```go= var q [3]int = [3]int{1,2} // [1,2,0] ``` 可以用 ... 代替長度,會自動以初始化的數量當長度 ```go= r := [...]int{1,2,3} ``` 陣列大小必須是常數運算式,在編譯時就能計算出的數字 陣列初始化可以用 index:value 的方式直接指定值,用這種方式索引不用按照順序,中間如果有省略的會用該型別的零值做初始化 ```go= type Currency int const ( USD Currency = iota EUR GBP ) symbol := [...]String{USD:"$", EUR:"€", GBP:"£"} // 0=>$, 1=>€, 2=>£ r := [...]int{9:-1} // 長度10 0~8=>0, 9=>-1 ``` 相同型別的陣列,如果元素可以比較,陣列就能直接用 `==` 比較 ```go= var a [2]int = [2]int{1,2} b := [...]int{1,2} var c [3]int = [3]int{} fmt.Println(a == b) // true fmt.Println(a == c) // compile error ``` go 呼叫函式時,每個參數的值拷貝被指派給對應的參數變數 - 使用這種方式傳遞大陣列沒效率 - 無法修改陣列 可以傳遞陣列指標來讓函式內的修改影響呼叫方 ```go= func zero(ptr *[32]byte) { *ptr = [32]byte{} } ``` 但缺點是 zero 就只能接受 `[32]byte` 的陣列,所以除非是固定的長度陣列,例如 sha256 固定大小的 hash (`[]byte`),不然就用 slice 吧 ### slice slice 是一個 struct ```go= type slice struct { array unsafe.Pointer // 指向存放資料的陣列指標 len int // 長度 cap int // 容量 } ``` - array 指標指向陣列可從 slice 存取的的第一個元素,但不一定是陣列的第一個元素 - len 為 slice 長度 - cap 為 array 第一個到底層陣列最後一個長度 (>= len) ``` 1,[2,3,4],5 ^ ptr slice[0] = 2 len(slice) = 3 cap(slice) = 4 ``` #### 切割 ```go= summer := month[6:9] // 6,7,8 ``` - 切割超過 len 會擴張 slice - 切割超過 cap 會引發 panic #### slice 的比較 陣列可以用 `==` 比較底層元素是否相同,slice 無法使用 `==` 比較,slice `==` 只能用來判斷 nil,不實做的原因是 1. slice 的元素是間接的,導致 slice 可能帶有本身;雖然有方法解決但不夠簡單且效率 2. 如果 slice 與其他參考型別(如:指標、channel)一樣,`==` 運算子用來測試參考對象是否相同,那 slice 與陣列在`==` 運算子上會有不同邏輯而導致困擾 3. 如果 slice 能用 `==` 運算子,則必須思考“slice 作為 map 的鍵”的情境(能用`==` 比較的都能用來當 map 的鍵),map 對鍵只進行淺複製,而在 map 的生命週期內每個鍵的相等性必須維持相同,但如果 slice 的底層陣列被修改,但參考不變,那 map 就無法維持鍵的相等性,因此 slice 不適合當 map 的鍵,那乾脆不要實作 `==` 標準函式庫提供了高度最佳化的 `bytes.Equal` 比較 `[]byte`,但如果是其他類型要自己實作 ```go= func equal(x, y []string) bool { if len(x) != len(y) { return false } for i := range x { if x[i] != y[i] { return false } } return true } ``` 可以用對 slice、struct、map 等使用 `reflect.DeepEqual()` 進行深度比較,比較二個結構體中的資料是否相同,但是這個效能很差,若不在意效能,例如測試時可以用 如果要測試 slice 是否為空,使用 `len(s)`,而非 `s == nil`,否則會遇到 ```go= s := []int(nil) // s == nil, len(s) == 0 s = []int{} // s != nil, len(s) == 0 ``` 在實作函數時,除非有特別說明,否則應該用相同方式對待 nil 和長度 0 的 slice 用內建 make 建構 slice 可以指定型別、長度、容量 ```go= make([]int, len) // 省略 cap, cap 會等於 len make([]int, len, cap) // make([]int, cap)[:len] 相同,slice 是陣列前 len 個元素的 view,但整個陣列容量是 cap ``` #### append 用 `appendInt()` 的概念來理解 append ```go= func appendInt(x []int, y int) []int { var z []int zlen := len(x) + 1 if zlen <= cap(x) { z = x[:zlen] } else { zcap := zlen if zcap < 2*len(x) { zcap = 2*len(x) } z = make([]int, zlen, zcap) copy(z, x) } z[len(x)] = y return z } ``` 通常不會知道 `append()` 會不會重新分配記憶體,所以會用回傳值指派給原本變數的方式處理 ```go= s = append(s, r) ``` #### slice 資料共享與記憶體重新分配 a 和 b 指向相同底層陣列,資料共享,a, b 修改會互相影響 ```go= a := make([]int, 5) a[0] = 1 a[1] = 2 // 1,2,0,0,0 b := a[1:4] b[1] = 9 // 1,9,0,0,0 ``` 使用 `append()` 時,cap 足夠,則會指向相同底層陣列 ```go= path := []byte("AAA/BBBBBBB") sepIndex := bytes.IndexByte(path, '/') a := path[:sepIndex] b := path[sepIndex+1:] // cap 足夠,a b 資料共享 a = append(a, "suffix"...) fmt.Println(string(a)) // AAAsuffix fmt.Println(string(b)) // uffixBB ``` 使用 Full slice expression,最後一個參數是 Limited Capacity,於是後續的 `append()` 時 cap 若不足,導致重新分配記憶體,指向一個新的且更大的底層陣列 ```go= path := []byte("AAA/BBBBBBB") sepIndex := bytes.IndexByte(path, '/') a := path[:sepIndex:sepIndex] // Limited Capacity b := path[sepIndex+1:] a = append(a, "suffix"...) fmt.Println(string(a)) // AAAsuffix fmt.Println(string(b)) // BBBBBBB ``` 原址 slice 的技巧 `nonempty()` 函式對一列字串過濾空值 ```go= func nonempty(str []string) []string { out := str[:0] for _, s := range str { if s != "" { out = append(out, s) } } return out } // 輸入與輸出共用底層陣列,節省空間 func nonempty(str []string) []string { i := 0 for _, s := range str { if s != "" { str[i] = s i++ } } return str[:i] } str := []string{"one", "", "three"} fmt.Println(nonempty(str)) // "one", "three" ``` #### slice 實作 stack ADT ```go= stack = append(stack, v) // push top = stack[len(stack)-1] // peek top = stack[:len(stack)-1] // pop ``` 移除第 i 個元素 ```go= func remove(slice[]int, i int) []int { copy(slice[i:], slice[i+1:]) return slice[:len(slice)-1] } // 不維持原本順序 func removeWithoutOrder(slice[]int, i int) []int { slice[i] = slice[len(slice)-1] return slice[:len(slice)-1] } func main() { s := []int{1,2,3,4,5} fmt.Println(remove(s, 3)) // 1,2,4,5 fmt.Println(removeWithUnorder(s, 3)) // 1,2,5,4 } ``` ### map 基本操作 ```go= ages := make(map[string]int) // 初始化 ages := map[string]int{ "Alice":25, "Ivy": 28, } delete(ages, "Alice") // 縮寫型式的指派 ages["Alice"] += 1 ages["Alice"]++ ``` map 在查詢不存在的鍵時回傳該型別的零值 ```go= ages["bob"] = ages["bob"] + 1 // bob 一開始不存在也沒問題 ``` 如果想知分辨 不存在的鍵和存在但是值為零的,可以用 ```go= age, ok := ages["Zoey"] if !ok { /* Zoey is not a key in this map */ } ``` 不能取得 map 元素的位址,因為 map 成長可能會將現有元素重新計算雜湊而改變位置,導致取得的位址無效 ```go= _ = &age["Alice"] ``` @todo map 迭代的順序不固定,故意設計成這樣是因為讓程式跨實作間更安全(? map 的零值為 nil,對 nil 的 map 的參考使用 `delete`、`len`、`range` 都是安全的,但對 nil 的 map 儲存會引發 panic ```go= var ages map[string]int fmt.Println(ages == nil) // true fmt.Println(len(ages)) // 0 ages["Su"] = 31 // panic ``` go 沒有 set,可以直接用 map 替代 ```go= seen := map[string]bool input := bufil.NewScanner(os.Stdin) for input.Scan() { line := input.Text() if !seen[line] { seen[line] = true fmt.Println(line) } } if err := input.Err(); err != nil { fmt.Fprintf(os.Stderr, "print %v\n", err) os.Exit(1) } ``` map 的 key 要是可以用 `==` 比較的型別,如果想用 slice 來當 key,可以透過輔助函式,先把 slice 轉成 key ```go= var m = make(map[string]int) func k (list []string) string { return fmt.Sprintf("%q", list) } func Add(list []string) { m[k(list)]++ } func Count(list []string) int { return m[k(list)] } ``` ### struct 基本用法 ```go= type Employee struct { ID int Name string Position string } var dilbert Employee // 透過點記號存取 dilbert.ID = 1 // 透過指標存取 position := &dilbert.Postion *position = "Senior " + *position // struct 指標也能用點記號 var employeeOfTheMonth *Employee = &dilbert employeeOfTheMonth.Position += " (proactive team player)" // 等於 (*employeeOfTheMonth).Position += " (proactive team player)" ``` 函式回傳指標,可以用點記號 ```go= func EmpolyeeByID(id int) *Employee { /* ... */ } Empolyee().Position = "senior" // 如果不是回傳指標會錯誤,因為左手邊不是變數 func EmpolyeeByID(id int) Employee { /* ... */ } Empolyee().Position = "senior" // compile error ``` struct S 不能帶有自己,但可以宣告 \*S,用來建構 tree、linked list 等遞迴資料結構 ```go= type tree struct { value int left, right *tree } ``` 宣告帶入欄位的值的方式有二種 ```go= type Employee struct { Position string } // 需要記住欄位順序,而且欄位順序之後也不方便改,通常用在比較小且固定順序的 // 如: image.Point{x, y}, color.RGBA{red, green, blue, alpha} albert := Employee{"senior"} // 這種比較常用,省略的欄位會以零值初始 dilbert := Employee{Position: "senior"} ``` 為了效率,較大的 struct 可以用指標傳入,或用指標回傳 或者要修改參數內的值,就必須用指標傳入 ```go= func Bouns(e *Employee, percent int) int { /**/ } ``` #### 比較 struct struct 會比較相對應的欄位,也可以當成 map 的鍵 ```go= type address struct { hostname string port int } hits := make(map[address]int) hits[address{"google.com", 443}]++ ``` #### 嵌入 struct 與不具名欄位 嵌入 struct ```go= // 抽取共同的欄位 type Point struct { X, Y int } type Circle struct { Center Point Radius int } type Wheel struct { Circle Circle Spokes int } var w Wheel // 但存取很麻煩 w.Circle.Center.X = 8 ``` 可以用不具名欄位,不具名欄位必須是具名型別或具名型別指標 ```go= // 抽取共同的欄位 type Point struct { X, Y int } type Circle struct { Point Radius int } type Wheel struct { Circle Spokes int } var w Wheel w.X = 8 ``` 缺點是宣告時也要用嵌入式的 ```go= var w = wheel{X:8, Y:8, Radius: 5, Spokes:20} // compile error var w = Wheel{ Circle: Circle{ Point: Point{X:8, Y:8}, Radius: 5, }, Spokes: 20, } ``` 不具名欄位其實是隱藏的欄位名稱,所以不能重複 ## 5. 函式 宣告 ```go= func add(x int, y int) int { return x + y } func sub(x, y, int) int { return x - y } func first(x int, _ int) int { return x } // 強調參數未使用 func zero(int, int) int { return 0 } fmt.Printf("%T\n", add) // "func(int, int) int" fmt.Printf("%T\n", sub) // "func(int, int) int" fmt.Printf("%T\n", first) // "func(int, int) int" fmt.Printf("%T\n", zero) // "func(int, int) int" ``` 函式的型別稱為 signature,參數、回傳值有相同型別與順序就是 signature 相同,不受參數名稱影響。 Go 沒辦法參數預設值 Go 的參數、具名變數、函式裡面第一層的區域變數屬於同一個區塊 Go 的參數是**傳值**的,除非參數帶有某種參考,像 slice、map、函式、channel,那呼叫方可能受到函式修改影響 沒有內容的函式宣告,表示是用其他語言實作 ```go= package math func Sin(x float64) float64 // 用組語實作 ``` ### 遞迴 取得 HTML 內所有 a href 連結的範例 `golang.org/x/` 提供網路、多與文字處理、行動平台;影像處理、加密等非標準套件,不在標準函示庫裡是因為比較少用 ```go= // golang.org/x/net/html func main() { doc, err := html.Parse(os.Stdin) if err != nil { os.Exit(1) } for _, link := range visit(nil, doc) { fmt.Println(link) } } func visit(links []string, n *html.Node) []string { if n.Type == html.ElementNode && n.Data == "a" { for _, a := range n.Attr { if a.Key == "href" { links = append(links, a.Val) } } } for c := n.FirstChild; c != nil; c = c.NextSibling { links = visit(links, c) } return links } ``` ### 多回傳值 需要多個參數時,可以直接用一個多回傳值替代,這在 debug 好用 ```go= // 二個方式一樣 log.Println(findlinks(url)) links, err := findlinks(url) log.Println(links, err) ``` 多個回傳值都屬於同一個型別時,命名會很重要 ```go= func Size(rect image.Rectabgle) (width, height int) func Split(path string) (dir, file string) func HoutMinSet(t time.Time) (hour, miunte, second int) ``` ### bare return 如果函式返回值有具名,return 陳述的運算子可以省略,優點是減少程式碼重複,但可讀性會變差,要去仔細看回傳值是初始值還是有重新指派值,建議 bare return 少用 ### 函式值 函式具有型別,可以指派給變數、當參數傳、從別的函式回傳 函式值讓我們可以將行為參數化 但函式不可比較,無法作為 map 的鍵 函式的零值是 nil,呼叫的話會引發 panic ### 匿名函式 重要的是內層函式可以參考到包圍本身的函式的變數 讓函式值能夠具有狀態 同時也表示為何函式屬於參考型別,與函式不可互相比較的原因 ```go= func squares() func () int { var x int return func () { x++ return x * x } } f := squares() fmt.Println(f()) // 1 fmt.Println(f()) // 4 fmt.Println(f()) // 9 fmt.Println(f()) // 16 ``` ### 擷取迭代變數的"錯誤" 因為迴圈內建構的函式值(匿名函式 slice) 內的 dir 變數被迴圈一直更新,而函式值又延遲到迴圈完成後才執行,所以最後變成都會輸出最後一個元素 c c c 這種錯誤常發生在 go routine、defer,因為這二種都會延後函式值的執行,在迴圈完成後才執行函式值 ```go= var dirs = []string{"a", "b", "c"} var rmdirs []func() for _, dir := range dirs { rmdirs = append(rmdirs, func() { fmt.Println(dir) }) } for _, rm := range rmdirs { rm() // c c c } ``` 類似的錯誤,i 變數也是被迴圈一直更新,最後輸出 c c c ```go= var dirs = []string{"a", "b", "c"} var rmdirs []func() for i := 0; i < len(dirs); i++ { rmdirs = append(rmdirs, func() { fmt.Println(dirs[i]) }) } for _, rm := range rmdirs { rm() // c c c } ``` 解決方法 ```go= var dirs = []string{"a", "b", "c"} var rmdirs []func() for _, dir := range dirs { dir := dir // 如果覺得看起來奇怪可以換變數名稱 rmdirs = append(rmdirs, func() { fmt.Println(dir) }) } for _, rm := range rmdirs { rm() // a b c } ``` ### 可變函式(參數) 表示可以用任意數量的該型別參數呼叫 在函式內的 vals 型別是 `[]int` ```go= func sum(vals ...int) int { total := 0 for _, val := range vals { total += val } return total } sum(1,2,3,4) // 參數已經是 slice 可以用省略符號 ... 呼叫可變函式 values := []int{1, 2, 3, 4} sum(values...) ``` 要注意的是雖然可變函式內的 vals 型別是 `[]int`,但是可變函式的型別和參數是真的 slice 的函式的型別不一樣 ```go= func f (...int) {} func g ([]int) {} fmt.Printf("%T\n", f) // func(...int) fmt.Printf("%T\n", g) // func([]int) var f func(...int) var a, b f a = func(x ...int) {} b = func(x []int) {} // compile error ``` ### 延遲函式呼叫 defer defer 通常和開啟關閉、連線斷線、上鎖解鎖等成對操作,確保資源釋放 defer 的位置最好是在資源取得後立刻發出 透過 defer 讓函式在被呼叫時輸出參數 ```go= func double(x int) (result int) { defer func() { fmt.Printf("double(%d) = %d\n", x, result) }() return x + x } ``` 在迴圈裡的 defer 要小心,因為延遲函式最後才會執行 ```go= for _, filename := range filenames { f, err := os.Open(filename) if err != nil { return err } defer f.Close() // 可能會耗盡檔案資源 } ``` 解決方法是把包括 defer 在內的程式移到每一輪呼叫的另一個函式內 ```go= for _, filename := range filenames { f, err := doFile(filename) if err != nil { return err } } func doFile(filename string) error { f, err := os.Open(filename) if err != nil { return err } defer f.Close() } ``` ### panic 如果在 run time 檢測到錯誤就會 panic panic 也能透過內建的 `panic()` 函式呼叫,通常用在**邏輯上不可能發生的狀況**,會導致程式中止,但預期中的錯誤(例如輸入錯誤)則應該使用 error 值處理 前置檢查是好習慣,但不需要做過頭,因為執行時就會知道錯誤了,除非可以提供更詳細錯誤訊息,來更快找出問題,否則這樣的檢查就沒意義 ```go= func reset (x *Buffer) { if x == nil { panic("x is nil") // 沒意義 } x.elements = nil } ``` 發生 panic 時,defer 會以反序執行 ### recover `recover()` 如果只會在 defer 內使用,並回傳 panic 值 - `recover()` 在 panic 前,回傳 nil - `recover()` 在 panic 後,已經 panic,執行不到 `recover()` 恢復錯誤通常是遇到意外後進行清理,例如遇到有問題的 server,可以關閉連線(開發環境可以回報錯誤),而不是讓客戶端等待 不應該恢復別的套件的 panic 而是應該回報錯誤 error 恢復同一個套件內的 panic 可以簡化複雜、未預期的錯誤,但也不該無差別的恢復,使得錯誤被忽略 選擇性恢復的範例 ## 6 方法 不是只有 struct 可以定義方法,同一個 package 的 slice、map、數字、字串、函式都可以,只要底層不是指標或介面即可 ```go= type Point struct{X, Y float64} type Path []Point perim := Path{ {1, 1}, {5, 1}, {5, 4}, } fmt.Println(perim.Distance()) ``` ### 指標接受器 指標不能當接受器 ```go= type P *int func (P) f() {} // compile error ``` 只有具名型別與具名型別的指標才能當接受器 ```go= // 具名型別的指標當接受器 func (p *Point) SacleBy(factor float64) { p.X *= factor p.Y *= factor } // 呼叫方式1 r := &Point{1, 2} r.ScaleBy(2) // 呼叫方式2 p := Point{1, 2} pptr := &p pptr.ScaleBy(2) // 呼叫方式3 s := Point{1, 2} (&s).ScaleBy(2) ``` 但其實編譯器會幫忙轉換,只要是變數呼叫方法,會根據方法的接受器自動轉換 - 方法定義是具名型別指標,用具名型別變數呼叫 => 轉換成具名型別指標 - 方法定義是具名型別,用具名型別變數指標呼叫 => 轉換成具名型別 ```go= func (p *Point) SacleBy(factor float64) { p.X *= factor p.Y *= factor } r := Point{1, 2} r.ScaleBy(2) // 自動轉換成 (&r).ScaleBy(2) func (p Point) SacleBy(factor float64) { p.X *= factor p.Y *= factor } p := Point{1, 2} pptr := &p pptr.ScaleBy(2) // 自動轉換成 (*pptr).ScaleBy(2) ``` ### 接受器是 nil 接受器的值可以是 nil 如果邏輯上允許接受器的值是 nil 時最好在文件註解寫一下(常發生在 slice、map 上,這時候的 nil 可能有特殊意義) ```go= type *IntList struct{ Value int Tail *IntList } // 如果是空鏈表回傳 0 func (list *IntList) Sum() int { if list == nil { return 0 } return list.Value + list.Tail.Sum() } type Values map[string][]string // 若無則回傳空字串 func (v Values) Get(key string) string { if vs := v[key]; len(vs) > 0 { return vs[0] } return "" } Vaules(nil).Get("item") // "" ``` ### 嵌入 struct 的組合型別 嵌入欄位可以是具名型別、具名型別指標、不具名型別、不具名型別指標 ```go= type Point struct{ X, Y float64 } type test struct { Color color.RGBA // 具名型別 color.RGBA // 不具名型別 P *Point // 具名型別指標 *Point // 不具名型別指標 } ``` 嵌入可以組合許多型別的方法 但這和 ColoredPoint **繼承** Point、color.RGBA 的概念不一樣,雖然 Point、color.RGBA 的方法會提升到 ColoredPoint,但是如果參數型別是 Point,是不能傳入 ColoredPoint 的 ```go= type ColoredPoint { Point Color color.RGBA } func (p Point) Distance(q Point) { /* ... */ } red := color.RGBA{255, 0, 0, 255} var p = ColoredPoint{Point{1, 1}, red} var q = ColoredPoint{Point{5, 6}, red} p.Distance(q.Point) // 還是要傳入 Point p.Distance(q) // compile error ``` 如果有重複的方法,首先會找接受器直接宣告的,再來會找 Point 提升一次的方法,(如果 Point 還嵌入其他的)再找提升二次的,如果有同等級的則會編譯錯誤 使用嵌入提升可讀性 ```go= var ( mu sync.Mutex mapping = make(map[string][string]) ) func Lookup(key string) string { mu.Lock() v := mapping[key] mu.Unlock() return v } // 重構 使用嵌入讓變數名稱更有意義 var cache = struct{ sync.Mutex mapping map[string]string }{ mapping: make(map[string]string), } func Lookup(key string) string { cache.Lock() v := cache.mapping[key] cache.Unlock() return v } ``` ### 方法值與運算式 #### 方法值 和函式值類似,方法也能產稱方法值,呼叫的時候就不用接受器 ```go= p := Point{1, 2} q := Point{4, 5} distanceFromP := p.Distance distanceFromP(q) // 5 ``` 方法值在一些套件的參數接受函式 API 呼叫很好用,參數可能剛好就是某個接受器的方法 ```go= type Rocket struct{ /* ... */ } func (r *Rocket) Launch() { /* ... */ } r := new(Rocket) time.AfterFunc(10 * time.Second, func() { r.Launch() }) // 可以這樣寫 更簡短 time.AfterFunc(10 * time.Second, r.Launch) ``` #### 方法運算式 直接用型別當接受器,**方法運算式會把原本的接受器的變數轉換成第一個參數** ```go= p := Point{1, 2} q := Point{4, 5} distance := Point.Distance // 方法運算式 distance(p, q) // 等於 p.Distance(q) ``` 利用方法運算式來透過某個值來選擇同一個接受器的不同方法 ```go= type Point struct{ X, Y float64 } func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} } func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} } type Path []Point func (path Path) TranslateBy(offset Point, add bool) { var op func(p, q Point) Point if add { op = Point.Add } else { op = Point.Sub } for i := range path { path[i] = op(path[i], offset) } } ``` ### 封裝 Go 透過第一個字母的大小寫來控制可見性,但**封裝單位是以 package 而不像其他語言是型別** 封裝的目的是隱藏資訊,有三個好處 1. 用戶無法直接修改屬性,所以很容易了解屬性可能的值 2. 設計者有更大的自由來修改實作方式,而不會破壞 API 相容性 3. 設計者能確保物件內部的不變性 在選擇要匯出或封裝時,可以參考以下幾點 1. 未來改動,例如是否會增加欄位 2. 影響的用戶程式碼數量 單純取得或修改型別內部值的函式稱為 `getter` `setter`,Go 語言習慣省略 Get 前綴(包括 Fetch、Find、Loopup 等) ###### tags: `tech`