# :memo: Golang ## Naming Rules and Conventions ### Files - 全小寫 snake case 用 _ 將多個字串起來 - 組合字用 _ 串起來 ex: hello_world - 開頭如果是 `.` or `_` 會被 go tool 略過 - 檔名尾是 `_test.go` 只給 go test tool 用的 (Unit test) ### Functions and Methods - camel case - 開頭 Upper case 表示 export 就是 public 可以被外面看到 - writeToDB // unexported, only visible within the package - WriteToDB // export, visible within the package ### Variables - camel case - 略縮全大寫 ex: serveHTTP, userID - 用單一字母 i, j, k - 複數詞表示 collection (slice, array) ex: songs, albums - 在同一 repo 使用相同的命名風格 - 避免重複 package name - log.Info() // good - log.logInfo() // bad ## Syntax ### declare and assignment ```go= var a, b, c int ``` ```go= var ( a int b int c int ) ``` ```go= var ( a, b, c int d, e, f bool ) ``` ```go= var a, b, c int = 1, 2, 3 ``` ```go= var a int = 1 var b int = 2 var c int = 3 ``` ```go= var ( a int = 1 b int = 2 c int = 3 ) ``` ```go= var ( a, b, c int = 1, 2, 3 ) ``` ```go= 只有在 func 能用語法糖,不寫型別 func main() { a := 1 b := 2 c := 3 d, e, f := true, 1, 30 } ``` ### Flow #### for > Go裡沒有`++i`, 也不能透過`i++`賦值給其他的變數 ```go= for i := 1; i <= 10; i++ { fmt.Println(i) } ``` ```go= // while (go 沒有 while) j := 1 for j <= 10 { fmt.Println(j) j++ } ``` ```go= // do while (go 沒有 do while) k := 1 for { fmt.Println(k) if k == 10 { break } k++ } ``` #### if ```go= if 7%2 == 0 { fmt.Println("7 is even") } else { fmt.Println("7 is odd") } if 8%4 == 0 { fmt.Println("8 is divisible by 4") } if num := 9; num < 0 { fmt.Println(num, "is negative") } else if num < 10 { fmt.Println(num, "has 1 digit") } else { fmt.Println(num, "has multiple digits") } ``` #### switch > switch 和其它語言不同,它不需要寫 break 這個關鍵字 ```go= // match i := 2 fmt.Print("Write ", i, " as ") switch i { case 1: fmt.Println("one") case 2: fmt.Println("two") case 3: fmt.Println("three") } ``` ```go= // match switch time.Now().Weekday() { case time.Saturday, time.Sunday: fmt.Println("It's the weekend") default: fmt.Println("It's a weekday") } ``` ```go= t := time.Now() switch { // expression like if else case t.Hour() < 12: fmt.Println("It's before noon") default: fmt.Println("It's after noon") } ``` ## Types & Keywords - [go types](https://golang.org/ref/spec#Numeric_types) | type | 的預設值 | value or ref |---|---|---| | integer | 0 | value | | floating | 0.0 | value | | bool | false | value | | string | "" | value | | interface | nil | ref | | slice | nil | ref | | channel | nil | ref | | map | nil | ref | | pointer | nil | ref | | function | nil | ref | | array | fileds zeroed | value | | struct | fileds zeroed | value | ### Nil > :bulb: 到底在 golang 中 nil 是什麼東西? > - nil 意思 - 意思是無 - 意思是 zero - 意思是零值 > - 如果 Declare 一個變數不進行賦值,那麼就會有個默認零值 - ex: var a int (a = 0) - ex: var b *int (a = nil) - ex: var c chan int (c = nil) - ex: var d map[string]string (d = nil) - ex: var e interface{} (e = nil) - ex: var f struct { A int B string C *int} (A=0, B="", C=nil) > - 所以 nil 是 **預定義** 的標識符,代表了 pointer, function, interface, map, slice, channel 的零值 > - 當 func 需要 pointer, function, interface, map, sliace, channel 時,如果不需要帶入內容,那可以直接給 nil > - ex: httpReq, _ := http.NewRequest(http.MethodGet, "https://postman-echo.com/get", nil) <- 這邊的 nil 是 io.Reader 表示沒有需要傳入內容 > - ex: 需要傳入 key -> value 的 map 時 (map[string]string),也可以使用 nil ,表示沒有對應的內容 ```go= // from source code /src/builtin/builtin.go type Type int var nil Type ``` > 它不是關鍵字,只是個變數,所以你可以改變 nil 的值,只是千萬不要這樣做 #### Nil type and compare ```go= package main import "reflect" func main() { var test any // interface{} 宣告未初始為 (interface{})(nil) println(test == nil) // true, 因為 nil 沒有型別,所以是一樣的 println(test == (*string)(nil)) // false test = (*string)(nil) println(test == nil) // false, 因為 test 是 (*string)(nil) println(test == (*string)(nil)) // true println(reflect.TypeOf(test).String()) // (*string) } ``` ### Constant - const 只能是以下類型 - bool - int, float, double - string - 可透過 const () group 賦值 - 可以 **只定義不用** - 如果 const group 中,如果不指定 type, value 則會和上一行非空值相同 - **iota** 是一個會自動被 compiler 累加的 const,而且它可以進行運算 ```go= const monday int = 1 const tuesday = "2" // group of const const ( one = 1 two = 2 three // const group 中沒定義值的話,和上一行一樣 four // 也是 2 ) const ( A = iota // 一個會被 compiler 自動累加的 const 從 0 開始 (int type) B C D ) const ( E = iota*2 + 1 // 可以參與計算 F G H ) // baseon golang 的 ENUM type memberLv int const ( memberV1 memberLv = iota + 1 // 結合 type 更能表現實際意義 memberV2 memberV3 memberV4 memberV5 ) func main() { fmt.Println(monday, tuesday) fmt.Println(three, four) fmt.Println(A, B, C, D) fmt.Println(E, F, G, H) fmt.Println(memberV5) } ``` ### String ```go= var s string = "" var ( s string = "" ) s := "" ``` ```go= s1 := "hello" s2 := "world" fmt.Println(s1+s2) // 透過 + 來連結字串 ``` ```go= // substring input := "this is hello world" fmt.Println(input[:4]) // from 0 get 3 char -> this fmt.Println(input[:len(input)]) // from 0 get full length of string -> this is hello world fmt.Println(input[:]) // 同上,取全部 // substring idx := strings.Index(input, "hello") // 傳回 hello 出挸的第一個位置,都沒找到回傳 -1 if idx != -1 { fmt.Println(idx, input[:idx]) // [0:8] result: 8 "this is " } ``` ### Bool ```go= var a bool = true var ( a bool = true ) a := true ``` ### Array > golang 中的 array 的變數不是個「address」真的是個變數 [看這篇](https://dwdraju.medium.com/deep-dive-into-pointers-arrays-slice-309a843c63ad) ```go= var a [5]int fmt.Println(a) a[4] = 100 fmt.Println(a) fmt.Println(a[4]) fmt.Println(len(a)) b := [5]int{ 1, 2, 3, 4, 5, } fmt.Println(b) var two [2][3]int for i := 0; i < 2; i++ { for j := 0; j < 3; j++ { two[i][j] = i + j } } fmt.Println(two) ``` ### Slice > 和 array 不同於,slice 透過 make 建立空間,且 slice 是可變的,可以動態改變長度,可透過 append 把元素加進去 > > 直接把 slice 看成是一種 struct 由 head pointer to array, len length (可視), cap length (實際大小) 組成 > > 透過 append() 這種會改變它的 struct {ptr, len, cap} 在傳遞變數到 func 要特別注意兩種方式 > 1. 傳「值」那就要回傳值,再把本來的 caller 的值蓋掉 > > ```go= > package main > > import "fmt" > > func modify(s []string) []string { > s = append(s, "c") > return s > } > > func main() { > mainS := []string{"a", "b"} > mainS = modify(mainS) > fmt.Printf("%+v", mainS) > } > ``` > > 2. 傳「址」就不用回傳值,func 進從 append 改變 slice struct 的結構 > > ```go= > package main > > import "fmt" > > func modify(s *[]string) { > *s = append(*s, "c") > } > > func main() { > mainS := []string{"a", "b"} > modify(&mainS) > fmt.Printf("%+v", mainS) > } > ``` ```go= s := make([]string, 3) // 先建出三格 string 的空間 s2 = append(s, "A") // 加入第四格並讓 s2 指向這個 slice 而 s 不變 ``` ```go= s := []string {} // 建出空的 string slice 空間 s2 = append(s, "A") // 加入第二格並讓 s2 指向這個 slice 而 s 不變 ``` ```go= s := make([]string, 3) s[0] = "a" s[1] = "b" s[2] = "c" fmt.Printf("%#v %d\n", s, len(s)) s_empty := []string { "d", "e", "f", } fmt.Printf("%#v %d\n", s_empty, len(s_empty)) s = append(s, "d") s = append(s, "e") fmt.Printf("%#v %d\n", s, len(s)) t := make([]string, len(s)) copy(t, s) fmt.Printf("%#v %d\n", t, len(t)) u := t[2:4] fmt.Printf("%#v %d\n", u, len(u)) v := t[:4] fmt.Printf("%#v %d\n", v, len(v)) w := t[3:] fmt.Printf("%#v %d\n", w, len(w)) twoD := make([][]int, 3) for i := 0; i < 3; i++ { twoD[i] = make([]int, i + 1) for j := 0; j < i; j++ { twoD[i][j] = i + j } } fmt.Println(twoD) ``` ```go= func update_array_pass_by_value(array [5]int) { array[1] = 5566 fmt.Println("update_array_pass_by_value:", array) } func update_array_pass_by_pointer(array *[5]int) { (*array)[1] = 5566 fmt.Println("update_array_pass_by_pointer:", *array) } func update_slice_pass_by_value(slice []int) { slice[1] = 7788 fmt.Println("update_slice_pass_by_value:", slice) } func update_slice_pass_by_pointer(slice *[]int) { (*slice)[2] = 9191 fmt.Println("update_slice_pass_by_pointer", *slice) } func main() { // array test a := [5]int{1,2,3,4,5} b := a fmt.Println(a, b) a[0] = 6 // change a to see b is changed ? fmt.Println(a, b) // no because b is "clone" from a, not "reference" from a // slice test c := []int{6,7,8,9,10} d := c fmt.Println(c, d) c[0] = 100 // change c to see d is chaned ? fmt.Println(c, d) // yes because d is "reference" from c, not "clone" from c update_array_pass_by_value(a) fmt.Println(a, b) // no change a and cloned b data update_array_pass_by_pointer(&a) fmt.Println(a, b) // a has changed, because pointer of the array is passed to function, function use the pointer dereferecing to array and change the data update_slice_pass_by_value(c) fmt.Println(c, d) // c and d has changed, because c,d,slice (is pointer) reference to the same data in memory update_slice_pass_by_pointer(&c) // c and d has changed, use double dereferencing to update the same data in memory. *slice -> *c -> []int{} fmt.Println(c, d) } ``` > slice 超雷 [參考](https://blog.golang.org/slices-intro#TOC_6.) > len = 可視 > cap = 真實存在的 array size > 當真實存在的 size 可以被 append 時,不會建另外一個 array 因此若每兩個 pointer 參考時會被互蓋 ```go= a := make([]int, 0, 10) // 做出 可視 len = 0 但 空間為 10 的 array fmt.Printf("a -> %#v %p len:%d cap:%d\n", a, a, len(a), cap(a)) b := append(a, 1,2,3) // 空間放的下,B 可視 len = 3 空間還是 10 的 array fmt.Printf("b -> %#v %p len:%d cap:%d\n", b, b, len(b), cap(b)) _ = append(a, 99, 88, 77) // 從 A 的位址開始往後蓋 3 格,對 A 沒影響,但影響了 B 的 可視 len fmt.Printf("a -> %#v %p len:%d cap:%d\n", a, a, len(a), cap(a)) fmt.Printf("b -> %#v %p len:%d cap:%d\n", b, b, len(b), cap(b)) /* a -> []int{} 0xc000020050 len:0 cap:10 b -> []int{1, 2, 3} 0xc000020050 len:3 cap:10 a -> []int{} 0xc000020050 len:0 cap:10 b -> []int{99, 88, 77} 0xc000020050 len:3 cap:10 */ fmt.Println("======================") aa := make([]int, 0, 2) // 做出 可視 len = 0 空間為 2 的 array fmt.Printf("aa -> %#v %p len:%d cap:%d\n", aa, aa, len(aa), cap(aa)) bb := append(aa, 5, 6, 7) // 要 append 時發現 空間為 2 不夠 copy ,因此做出另外一個 array = cap*2 = 4 的空間 // 把 5, 6, 7 co 過去,讓 bb 指向開頭,因此 aa, bb 的地址不同了,因為指向兩個不同的實體 fmt.Printf("bb -> %#v %p len:%d cap:%d\n", bb, bb, len(bb), cap(bb)) _ = append(aa, 11, 22, 33) // aa 的空間為 2 的 array 還是不夠放,因此要建另外個 array ,但是和 bb 完全沒關係 fmt.Printf("aa -> %#v %p len:%d cap:%d\n", aa, aa, len(aa), cap(aa)) fmt.Printf("bb -> %#v %p len:%d cap:%d\n", bb, bb, len(bb), cap(bb)) /* aa -> []int{} 0xc000014110 len:0 cap:2 bb -> []int{5, 6, 7} 0xc00001a080 len:3 cap:4 aa -> []int{} 0xc000014110 len:0 cap:2 bb -> []int{5, 6, 7} 0xc00001a080 len:3 cap:4 */ ``` > 宣告和初始化,Nil slice != empty slice 喔 ```go= var a []int // nil 用在切始不知道有多少數量的 slice a := make([]int, 0, N) // 當已知 range 的數量時可以用 make 預先配置 cap 較好效能 a := []int{1,2} // 當已知元素要塞時,透過初始塞入進行配置 ``` ```go= var foo []string // nil slice var bar = []string{} // empty slice fmt.Println(foo == nil) // true fmt.Println(bar == nil) // false for _, v := range foo { // no errors fmt.Println("nothing, no print but no error", v) } for _, v := range bar { // no errors fmt.Println("nothing, no print but no error", v) } ``` ```go= func remove(slice []int, idx int) []int { return append(slice[:idx], slice[idx+1:]...) } func main() { var s1 = []int{1,2,3,4,5} fmt.Println(remove(s1, 4)) } ``` ### Map > 像是 python dict 也就是 key,value 的 mapping,當 key 不存在的時候,會噴出 "" 空字串 > > 可以透過 delete 刪除 key > > 本身 map variable 就是 pointer(reference 存地址) 因此不需要再使用 pointer 去指向該 instance 直接透過 n := m assign 就可以參考相同的結構 > > **它是無序的,所以每次執行的走訪結果也是不同的,要特別注意** ```go= m := []map[string]map[string]string { // list of dict of dict { "a": { "a": "ok", }, "b": { "b": "ok", }, }, } fmt.Println(m) n := []map[string]string { // list of dict { "a": "a" }, { "b": "a" }, { "c": "a" }, } fmt.Println(n) n[0] = map[string]string {"d":"b"} // assign array[0] new dict fmt.Println(n) m[0]["a"]["c"] = "hello" m = append(m, map[string]map[string]string { "ok": { "tt": "hello", }, }) fmt.Println(m) delete(m[0]["a"], "a") fmt.Println(m, len(m), len(m[0]["a"])) result, ok := n[1]["c"] fmt.Println(result, ok) // "", false ``` > 宣告和初始化,Nil map != empty map 喔 ```go= var m map[string]int // nil 用在指向某個同型別的 map,沒初始化不能操作使用 m := make(map[string]int) // 初始化為空map m := map[string]int{"10":10} // 給值初始化 ``` ```go= m := map[string]string{ "abc": "124", "def": "456", } n := m fmt.Printf("%+v %+v\n", m, n) m["abc"] = "789" fmt.Printf("%+v %+v\n", m, n) n["hello"] = "world" fmt.Printf("%+v %+v\n", m, n) o := &m // no need to use pointer to reference map data fmt.Printf("%+v %+v\n", m, *o) (*o)["kkk"] = "100" // equal n := m, n["kkk"] = "100" fmt.Printf("%+v %+v\n", m, *o) ``` ### Channel > 透過 channel 做 **等待** 用,或是 **event** 通知用 > > 因為 make(chan string) size = 0 為 unbuffered channel 所以行為像 **接力棒** 要給棒的,要等拿棒的,而要拿棒的要等給棒的,所以阻塞行為會發生在 > - 塞入時阻塞,等待到被拉取才能離開 > - 拉取時阻塞,等待到有被塞入才能離開 > > 當所有 goroutine 都無法滿足 channel 而執行完畢時,則發生 deadlock ```go= const ( DONE = "done" ) // say 做 5 秒後發出 event DONE func say(s string, e chan<- string) { for i := 1; i <= 5; i++ { fmt.Println(i, s) time.Sleep(1 * time.Second) } e <- DONE } func main() { event := make(chan string) // init block chan (size zero) go say("dachi", event) time.Sleep(3 * time.Second) // 睡 3 秒啟動下個 say go say("mike", event) fmt.Println("main do pull event first") // 3 秒後印出 fmt.Println("main event pulled first", <-event) // 5 秒後 say("dachi") 發出 event 後,才能從阻塞中拿到 event fmt.Println("main do pull event second") // 5 秒後印出 fmt.Println("main event pulled second", <-event) // 8 秒後 say("mike") 發出 event 後,才能從阻塞中拿到 event } // Result // 1 dachi // 2 dachi // 3 dachi // main do pull event first // 1 mike // 4 dachi // 2 mike // 5 dachi // main event pulled first done // main do pull event second // 3 mike // 4 mike // 5 mike // main event pulled second done ``` > 使用 channel 當 **變數** 做為多執行緒下共享變數,避免 **變數競爭** 狀態,設定 ```go= total := make(chan int, 1) 只有一格的 buffered channel ``` > 當 goroutine 讀取 channel 時,若 channel 有值,則可以取得,當取得後迫使其它 goroutine 也要拿 channel 的值時,因為無值必須等待,而當寫回去的瞬間,其它等待 channel 要拿取的 goroutine 可以啟動,但只有一個 goroutine 能拿到值,如此可以 **保證** channel 中的值,一次只能被一個 goroutine 存取 (拿出再寫回) > > 下面 case1, case2 行為完全不同,case1 是預料外的結果,而 case2 是正常的,因為 case1 當 goroutine 發出去時,瞬間 main goroutine 已經把 i 加到 11 了,所以當發出去的 goroutine 拿取 i 時是 11 > > case2 是透過 func paramter 來 **copy value** 來保證 goroutine 運算中用到的值,是 **啟動** goroutine 時 **傳入** 的計算值 ```go= func main() { var wg sync.WaitGroup total := make(chan int, 1) // case 1 total <- 0 for i := 1; i <= 10; i++ { wg.Add(1) go func() { defer wg.Done() fmt.Println("case1, i:", i) total <- <-total + i }() } wg.Wait() fmt.Println(<-total) // case 2 total <- 0 for i := 1; i <= 10; i++ { wg.Add(1) go func(num int) { defer wg.Done() fmt.Println("case2, i:", num) total <- <-total + num }(i) } wg.Wait() fmt.Println(<-total) } // Result // case1, i: 11 // case1, i: 11 // case1, i: 11 // case1, i: 11 // case1, i: 10 // case1, i: 11 // case1, i: 11 // case1, i: 11 // case1, i: 8 // case1, i: 4 // 110 // case2, i: 1 // case2, i: 10 // case2, i: 2 // case2, i: 7 // case2, i: 5 // case2, i: 9 // case2, i: 8 // case2, i: 4 // case2, i: 6 // case2, i: 3 // 55 ``` > - unbuffered channel > - 等待用 > - 接力棒 > - 推/拉 相互等待 > - buffered channel > - 重點在 channel buffer 空或是滿 > - 空時,拉資料會 block > - 滿時,推資料會 block > - 要自己控好 buffer > - 讀取 channel 時 > - 重點在 channel close > - close 為讀取 channel 的終止條件 > - for infinity or range 都可以以 close 為條件讀取全部 ```go= func main() { // unbuffered channel // main goroutine wait the other goroutine ch1 := make(chan string) go func() { fmt.Println("first goroutine running.") time.Sleep(3 * time.Second) ch1 <- "done" }() fmt.Println("main start to wait...") fmt.Println("main get channel string", <-ch1) // buffered channel ch2 := make(chan string, 10) for i := 0; i < 10; i++ { go func(num int) { fmt.Printf("%d go routine running.\n", num) time.Sleep(3 * time.Second) // every goroutine running 3s fmt.Printf("%d go routine done.\n", num) ch2 <- fmt.Sprintf("%d Done.", num) }(i) } // after 5s close the channel time.Sleep(5 * time.Second) close(ch2) for data := range ch2 { fmt.Println("Get channel result,", data) } _, ok := <-ch2 if !ok { fmt.Println("channel is closed.") } // or for to watch ch2 data // for { // data, ok := <-ch2 // if !ok { // fmt.Println("channel is closed") // } // fmt.Println("result,", data) // } // Result // main start to wait... // first goroutine running. // main get channel string done // 1 go routine running. // 9 go routine running. // 6 go routine running. // 7 go routine running. // 3 go routine running. // 2 go routine running. // 4 go routine running. // 5 go routine running. // 0 go routine running. // 8 go routine running. // 8 go routine done. // 5 go routine done. // 1 go routine done. // 4 go routine done. // 9 go routine done. // 6 go routine done. // 2 go routine done. // 7 go routine done. // 3 go routine done. // 0 go routine done. // Get channel result, 8 Done. // Get channel result, 5 Done. // Get channel result, 1 Done. // Get channel result, 4 Done. // Get channel result, 9 Done. // Get channel result, 6 Done. // Get channel result, 2 Done. // Get channel result, 7 Done. // Get channel result, 0 Done. // Get channel result, 3 Done. // channel is closed. } ``` > 透過 select 來處理 channel block 的狀態 > - 當 channel 無法讀取時,default 處理 > - 當 channel 可讀取時,處理 channel 的資料 > select 本身可以當成阻塞方法,防止 main routine 離開 > ```go= > for {time.Sleep(time.Second)} > ``` > 等同於 > ```go= > select{} > ``` ```go= func main() { ch := make(chan string) go func() { fmt.Println("Goroutine: start running...") // do some heavy computing job time.Sleep(10 * time.Second) fmt.Println("Goroutine: run done...") ch <- "done" }() // wait goroutine every second for { select { case result := <-ch: fmt.Println("Mainroutine: check goroutine is", result) goto END default: time.Sleep(1 * time.Second) fmt.Println("Mainroutine: goroutine not complete, wait for 1 second") } } END: } ``` ```go= // 時限內,展開 10 個工人處理,並進行匯總結果 func run(ID int, result chan<- map[int]string, wg *sync.WaitGroup) { defer wg.Done() t := time.Now().UnixNano() rand := rand.New(rand.NewSource(t)) sleepInSec := rand.Intn(10) fmt.Printf("%d goroutine will sleep in %d seconds, start ...\n", ID, sleepInSec) time.Sleep(time.Duration(sleepInSec) * time.Second) fmt.Printf("%d goroutine sleep %d done.\n", ID, sleepInSec) result <- map[int]string { ID: fmt.Sprintf("sleep %d secs", sleepInSec), } } func main() { var wg sync.WaitGroup resultChan := make(chan map[int]string) finishChan := make(chan string) timesUpChan := make(chan bool) for i := 0; i < 10; i++ { wg.Add(1) go run(i, resultChan, &wg) } go func() { fmt.Println("start to watch all routine...") wg.Wait() finishChan <- "Done" }() go func() { fmt.Println("start the timer, and wait for 7 seconds.") <-time.After(time.Duration(7) * time.Second) timesUpChan <- true }() resultData := map[int]string{} Loop: for { select { case result := <-resultChan: for k, v := range result { resultData[k] = v } case <-finishChan: fmt.Println("all routine done.") for i := 0; i < len(resultData); i++ { fmt.Printf("routine %d %s\n", i, resultData[i]) } break Loop case <-timesUpChan: fmt.Println("times up.") break Loop } } } ``` ```go= // 測試拉取,防止卡住 func main() { messages := make(chan string) signals := make(chan bool) select { // 看 case 的當下立刻「選擇」處理 case msg := <-messages: //沒東西等待拉取,跳過 fmt.Println("received message", msg) default: fmt.Println("no message received") } msg := "hi" select { case messages <- msg: //沒人等著接收,跳過 fmt.Println("sent message", msg) default: fmt.Println("no message sent") } } ``` > 透過 select 可以測試 channel 並防止 blocking (塞的進去就塞,塞不進去就跳過。拉的出來就拉,拉不出來就跳過) ```go= func main() { type Book struct{id int} bookshelf := make(chan Book, 3) for i := 0; i < cap(bookshelf) * 2; i++ { select { case bookshelf <- Book{id: i}: fmt.Println("succeeded to put book", i) default: fmt.Println("failed to put book") } } for i := 0; i < cap(bookshelf) * 2; i++ { select { case book := <-bookshelf: fmt.Println("succeeded to get book", book.id) default: fmt.Println("failed to get book") } } } /* result succeeded to put book 0 succeeded to put book 1 succeeded to put book 2 failed to put book failed to put book failed to put book succeeded to get book 0 succeeded to get book 1 succeeded to get book 2 failed to get book failed to get book failed to get book */ ``` ```go= // 第一個完成 func source(c chan<- int32) { ra, rb := rand.Int31(), rand.Intn(3) + 1 // Sleep 1s/2s/3s. time.Sleep(time.Duration(rb) * time.Second) c <- ra } func main() { rand.Seed(time.Now().UnixNano()) startTime := time.Now() // c must be a buffered channel. c := make(chan int32, 5) for i := 0; i < cap(c); i++ { go source(c) } // Only the first response will be used. rnd := <-c fmt.Println(time.Since(startTime)) fmt.Println(rnd) } ``` ```go= // 同上,透過 select 來抓 func source() <-chan int32 { c := make(chan int32, 1) // 必須有一格,以防止塞不要進去 go func() { ra, rb := rand.Int31(), rand.Intn(3)+1 time.Sleep(time.Duration(rb) * time.Second) c <- ra }() return c } func main() { rand.Seed(time.Now().UnixNano()) var rnd int32 // 阻塞到任一個 source 可以拉到資料為止 (拿第一個完成的) select{ case rnd = <-source(): case rnd = <-source(): case rnd = <-source(): } fmt.Println(rnd) } ``` ```go= // 限時內的請求 func requestWithTimeout(timeout time.Duration) (int, error) { c := make(chan int) go doRequest(c) // 可能會超出預期的時間 select { case data := <-c: return data, nil case <-time.After(timeout): return 0, errors.New("timeout!!!") } } ``` ```go= // 一定速率進行請求 type Request interface{} func handle(r Request) {fmt.Println(r.(int))} const RateLimitPeriod = time.Minute const RateLimit = 200 // 一份鐘內處理 200 個請求 func handleRequests(requests <-chan Request) { // 先分配好請求的計時器要有多少個 quotas := make(chan time.Time, RateLimit) // 算出速率啟動 ticker 讓每速率下啟動塞入 quotes go func() { tick := time.NewTicker(RateLimitPeriod / RateLimit) defer tick.Stop() for t := range tick.C { // 每 tick 一次就試著塞看看 select { case quotas <- t: default: // 塞不了,就不做事等待下次 tick } } }() for r := range requests { <-quotas // 拿出已分配好的 slot 它已經是被 tick 分段好的 queue 了 go handle(r) } } func main() { requests := make(chan Request) go handleRequests(requests) // time.Sleep(time.Minute) for i := 0; ; i++ {requests <- i} } ``` > channel close 很重要,才能判斷結束執行 > ==只有 channel 關閉了,去拉才「永遠」不會卡住,但塞進一個 close channel 會 panic== > ```go= > job, ok := <-jobs // 當 channel closed, ok 會是 false > // ok = true "channel 未關閉,值有效" > // ok = false "channel 已關閉,值為 zero value" > ``` > 關閉一個 nil channel 會 panic > ```go= > var c chan int > close(c) > ``` > 行為公式表 > | Operation | Nil Channel | Closed Channel | 一般 Channel | > | -------- | -------- | -------- | -------- | > | Close | panic | panic | 成功關閉 | > | 塞值進去 | 卡住 (block) | panic | 卡住或成功塞入 | > | 拉值出來 | 卡住 (block)| 永遠拿出 (0,false) 且不會卡住 (never block) | 卡住或成功讀取 | ```go= // 1 producer, 1 comsumer, N jobs func worker(jobs <-chan int, done chan<- string) { for { job, ok := <-jobs //等同於 job := range jobs if ok { fmt.Println("receive job", job) } else { fmt.Println("no available job") done <- "done" break } } } func main() { jobs := make(chan int, 10) event := make(chan string) go worker(jobs, event) // start comsumer time.Sleep(time.Duration(3) * time.Second) for i := 1; i <= 5; i++ { jobs <- i // produce job to channel time.Sleep(time.Duration(1) * time.Second) fmt.Println("set job", i, "to jobs") } close(jobs) // close 是終止條件,非常重要 <-event // wait to all jobs done } /* result receive job 1 <- worker (goroutine) set job 1 to jobs <- produce (mainroutine) receive job 2 set job 2 to jobs receive job 3 set job 3 to jobs receive job 4 set job 4 to jobs receive job 5 set job 5 to jobs no available job */ ``` ```go= // 1 producer, 3 comsumer, N jobs const ( NUM_JOBS = 10 BOT = 3 ) func bot(id int, jobs <-chan int) { fmt.Println("bot", id, "wakeup.") for job := range jobs { // range 會 block until channel close // 就算 jobs 有多個資料,每次只會拿出一個 // 而且保證競爭取得 channel 內容時,保證只有一個 routine 會拿到 fmt.Println("bot", id, "started job", job) time.Sleep(time.Duration(3) * time.Second) fmt.Println("bot", id, "finished job", job) } fmt.Println("bot", id, "exit.") } func main() { jobs := make(chan int, NUM_JOBS) // 其實用 unbuffered 也行,只要一生產就馬上被消費 for i := 1; i <= BOT; i++ { go bot(i, jobs) } // bot 3 wakeup. // bot 1 wakeup. // bot 2 wakeup. time.Sleep(3 * time.Second) jobs <- 1 // bot 3 started job 1 jobs <- 2 // bot 1 started job 2 time.Sleep(3 * time.Second) // bot 3 finished job 1 // bot 1 finished job 2 jobs <- 5 // bot 2 started job 5 jobs <- 6 // bot 3 started job 6 jobs <- 7 // bot 1 started job 7 time.Sleep(2 * time.Second) // bot 1 finished job 7 jobs <- 9 // bot 1 started job 9 close(jobs) // 當 close chan 時,才可以讓 range 離開,producer 必須做到關閉 channel 責任 // bot 2 finished job 5 // bot 2 exit. // bot 3 finished job 6 // bot 3 exit. // bot 1 finished job 9 // bot 1 exit. <-time.After(30 * time.Second) // 時間到離開 main routine } ``` > 養成好習慣,當 channel close 之後設為 nil ```go= // 透過 select 讀取關閉的 channel 要設為 nil 讓它 block forever func main() { a := make(chan int) b := make(chan int) go func() { time.Sleep(5 * time.Second) a <- 10 }() go func() { time.Sleep(8 * time.Second) a <- 8 }() go func() { time.Sleep(10 * time.Second) fmt.Println("close chan a") close(a) // 關閉 channel 會導致 <-a 的處理總是拿到 0, false }() for { select { case v, ok := <-a: if !ok { a = nil // 當發現 chan a 被關閉了,設為 nil 讓 chan a 永遠阻 // 如果沒設為 nil 每次 select 都會進到這邊,就開始燒 CPU 了 } else { fmt.Println("a", v) // 理論上關閉 chan 不能再有 chan 的讀取,需要以 !ok 為條件,作為終結 } case v, ok := <-b: if !ok { b = nil } else { fmt.Println("b", v) } case <-time.After(3 * time.Second): fmt.Println("no data") } } } ``` ```go= // 展開同時最多幾個 Job limit doingJobQueue = make(chan struct{}, maxConcurrent) go loop() func loop() { for { doingJobQueue <- struct{}{} // 有空間可塞入,可同時處理最多 max job,塞不進去就 block 直到有 job 做完 go func() { handleJob(msg) <-doingJobQueue // job done 從 queue 中排出 } } } ``` ```go= // stateful goroutines by channel type writeOp struct { key int val int ok chan bool } func main() { writes := make(chan writeOp) state := make(map[int]int) stop := false go func() { for { select { case write := <-writes: // 2. 拉取 writes queue 進行處理 state[write.key] = write.val write.ok <- true // 3. 處理完成送個訊號 } } }() for w := 0; w < 10; w++ { go func() { // write generator for { write := writeOp{ key: rand.Intn(5), val: rand.Intn(100), ok: make(chan bool), } writes <- write // 1. 丟進 writes 等待被處理 <-write.ok // 4. 等待處理完成的訊號,再往下做 if stop { fmt.Println("receive stop") return } } }() } <-time.After(time.Duration(5) * time.Second) stop = true <-time.After(time.Duration(3) * time.Second) fmt.Println("state:", state) } ``` ```go= // Fan-out func worker(in <-chan int, out chan<- int) { for data := range in { // Process data result := processData(data) out <- result } } // Fan-in func merge(channels []<-chan int, out chan<- int) { var wg sync.WaitGroup for _, ch := range channels { wg.Add(1) go func(ch <-chan int) { defer wg.Done() for data := range ch { out <- data } }(ch) } wg.Wait() close(out) } ``` ```go= // fan-out and fan-in func main() { input := make(chan int) result := make(chan int) data := []int{} sig := make(chan struct{}) go func() { for i := range result { // fan-in collect result to data data = append(data, i) } sig <- struct{}{} // 資料整理完成後送 sig }() wg := sync.WaitGroup{} for i := 0; i < 3; i++ { wg.Add(1) go worker(i, input, &wg, result) // fan-out worker } for i := 0; i < 10; i++ { // produce data input <- i } close(input) // end data input wg.Wait() // all workers are done close(result) // exit collect <-sig // 等待資料整理完成 sum := 0 for _, i := range data { sum += i } fmt.Println("done", sum) } ``` ```go= // ping pong goroutine type sig struct{} func main() { i := 1 a := make(chan sig) b := make(chan sig) wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() for { <-a fmt.Println("a:", i) i++ if i > 10 { select { case b <- sig{}: // if can write default: // skip } fmt.Println("a exit") return } b <- sig{} } }() wg.Add(1) go func() { defer wg.Done() for { <-b fmt.Println("b:", i) i++ if i > 10 { select { case a <- sig{}: // if can write default: // skip } fmt.Println("b exist") return } a <- sig{} } }() a <- sig{} wg.Wait() fmt.Println("all done.") } ``` ```go= // errgroup 把所有平行處理都丟進去,只抓致少一個 Error // eg.Wait() 會等待結束,並回傳有任何 error 的狀況 func (y *YahooFinanceService) GetMixedQuote() (finance.MixedQuoteData, error) { eg := errgroup.Group{} forexQuotes := []finance.Quote{} indicatorQuotes := []finance.Quote{} eg.Go(func() error { q, err := y.GetForexs([]finance.ForexExchange{ { FromUint: currency.USD, ToUnit: currency.TWD, }, { FromUint: currency.JPY, ToUnit: currency.TWD, }, }) if err != nil { return fmt.Errorf("get forex data from finance service failed. %w", err) } forexQuotes = q return nil }) eg.Go(func() error { q, err := y.GetIndicators( []string{"^GSPC", "^DJI", "^IXIC"}, ) if err != nil { return fmt.Errorf("get indicator data from finance service failed. %w", err) } indicatorQuotes = q return nil }) err := eg.Wait() data := finance.MixedQuoteData{ Indicators: indicatorQuotes, Forexs: forexQuotes, } return data, err } ``` ```go= // semaphore 固定展開一定數量平行處理 // 搭配 waitGroup 做所有的 routine 都完成的控制 func main() { data := []string{ "ok1", "ok2", "ok3", "ok4", "ok5", "ok6", "ok7", "ok8", "ok9", "ok10", } wg := sync.WaitGroup{} sem := make(chan struct{}, 3) for _, d := range data { sem <- struct{}{} // acquire a slot wg.Add(1) go func() { defer func() { <-sem // release a slot wg.Done() }() time.Sleep(time.Second) fmt.Println("go routine say:", d) }() } wg.Wait() fmt.Println("all finished") } ``` ```go= // 生產消費模型,消費者平行消費 // 透過 done 來確認完成消費都完成了 func producer(ch chan<- string, amount int) { for i := range amount { msg := fmt.Sprintf("%d, ok", i) ch <- msg fmt.Println("produce:", msg) time.Sleep(200 * time.Millisecond) } close(ch) } func consumer(ch <-chan string, done chan<- struct{}) { wg := sync.WaitGroup{} sem := make(chan struct{}, 3) for data := range ch { sem <- struct{}{} // acquire a solt wg.Add(1) go func() { defer func() { <-sem // release a solt wg.Done() }() time.Sleep(time.Second) fmt.Println("go routine say:", data) }() } wg.Wait() done<-struct{}{} } func main() { ch := make(chan string, 20) done := make(chan struct{}) go producer(ch, 10) go consumer(ch, done) fmt.Println("start wait for process") <-done fmt.Println("all finished") } ``` ```go= // 透過 timeout 強制關閉,對有 graceful shutdown 的服務來操作 package main import ( "fmt" "time" ) func main() { timeout := time.After(2 * time.Second) ch := make(chan struct{}) go func() { time.Sleep(1 * time.Second) ch <- struct{}{} }() select { case <-timeout: fmt.Println("force cancel") case <-ch: fmt.Println("graceful stop") } fmt.Println("done") } ``` ```go= // 透過 timer 由 routine 來做 force cancel package main import ( "fmt" "time" ) func main() { timer := time.AfterFunc(2*time.Second, func() { fmt.Println("force cancel") }) defer timer.Stop() // 如果 graceful stop 提前結束就停止 timer fmt.Println("graceful stop") serverGracefulStop() // 模擬 graceful stop ,當 2 秒後 force shutdown 這邊將會返回 fmt.Println("done") } func serverGracefulStop() { time.Sleep(5 * time.Second) } ``` #### **close(ch) vs ch <- struct{}{}** > 發送資料 (serverClosed <- struct{}{}):需要有協程在接收該資料,否則發送操作會阻塞。這種方式適合==一對一==的通知。 > 關閉通道 (close(serverClosed)):所有正在等待接收該通道資料的協程都會被喚醒,接收到零值。這種方式適合==一對多==的通知。 #### close ch > 透過 close channel 可以讓等待的 channel routine 立刻返回 ==exit== ```go= package main import ( "fmt" "time" ) func main() { ch := make(chan int) go func() { ch <- 100 time.Sleep(1 * time.Second) fmt.Println("done") close(ch) // 必須有中止 channel 的條件 }() // close 是離開 channel 的方法 for v := range ch { // 印到 channel 沒資料 fmt.Println(v) } } ``` #### for if ok channel > 透過 ==if c, ok := <-ch; ok {}== 可以判斷 channel 有沒有關閉,如果是 ==關閉== ok 會是 ==false== 告訴你該 channel 已經關閉了 ```go= package main import ( "fmt" "time" ) func main() { ch := make(chan int) go func() { ch <- 100 time.Sleep(1 * time.Second) fmt.Println("done") close(ch) // 必須有中止 channel 的條件 }() for { if v, ok := <-ch; ok { fmt.Println(v) } else { // 當 channel 關閉時, ok 為 false break } } } ``` ### Byte & Rune > utf-8 會用 3 Byte 來表示,而用一個 1 Rune 表示 > > []string 用 range 來 iter 會是 Rune (符號)表示,但是用 len 實際看空間是用 bytes 計算 ```go= str := "hello 三民主義,吾黨所宗101" // utf-8 3 bytes [a-z][0-1] 1 byte b := []byte("hello 三民主義,吾黨所宗101") r := []rune("hello 三民主義,吾黨所宗101") fmt.Println(str, len(str)) // 36=6+27+3 fmt.Println(b, len(b)) // 36=6+27+3 fmt.Println(r, len(r)) // 符號,所以 18=6+9+3 charCount := 0 for _, c := range str { // range 會以符號來佚代,但用 Len 是以 bytes 計算 fmt.Println(c) charCount++ } fmt.Println("===========") for _, data := range b { fmt.Println(data) } fmt.Println("===========") for _, data := range r { fmt.Println(data) } fmt.Println("===========") fmt.Println(charCount, len(str)) // rune 18 bytes 36 ``` ```go= str := "三民主義,吾黨所宗" // 要計算有幾個字符,要先轉 rune rstr := []rune(str) cnt := len(rstr) fmt.Println(cnt) // 9 // 要記算有幾個 bytes 要先轉 byte bstr := []byte(str) cnt = len(bstr) fmt.Println(cnt) // 8*3+1 = 25 ``` ```go= // compare rune func main() { f, _ := os.Open("./../day3.txt") defer f.Close() scanner := bufio.NewScanner(f) totalPriority := 0 for scanner.Scan() { line := scanner.Text() mid := len(line) / 2 // ex: 22 / 2 = 11 (11個) part1 := line[:mid] part2 := line[mid:] item := findTheItem(part1, part2) priority := getItemPriority(item) fmt.Println(line, part1, part2, string(item), item, priority) totalPriority += priority } fmt.Println("totalPriority:", totalPriority) } func getItemPriority(item rune) int { if item > 97 { // lowercase return int(item) - 97 + 1 } else { // uppercase return int(item) - 65 + 27 } } func findTheItem(left string, right string) rune { for _, i := range left { for _, j := range right { if i == j { return i } } } return rune(0) // zero rune, zero value = int32 } ``` ### Struct > 就像 Array 一樣,一個 Struct 的變數存的不是 pointer 而是實際的結構 > > 所以透過 a := b 的指定是有 copy 的,而不是 copy address > > 可以透過 pointer.field or (*pointer).field 來存取,golang 會自動 dereferecing pointer ```go= var p Person //不是 nil 已經被初始化了,配置了預設值 ``` ```go= type T struct { n int f float64 next *T } fmt.Println([2]T{}) // [{0 0 <nil>} {0 0 <nil>}] ``` ```go= type Person struct { Name string Age int } func updatePerson(p *Person, newName string) *Person { p.Name = newName // equal to (*p).Name = newName return p } func main() { p := Person{ // like array Name: "Dachi", Age: 30, } fmt.Printf("%+v\n", p) p.Age = 10 fmt.Printf("%+v\n", p) s := &p // s is pointer to struct (*s).Age = 11 // use pointer to update struct data fmt.Printf("%+v\n", p) s.Age = 12 // the pointers are automatically dereferenced. fmt.Printf("%+v\n", p) p2 := p // data copy not pointer copy fmt.Printf("%+v %+v\n", p, p2) p.Name = "Max" // change p will not change p2 fmt.Printf("%+v %+v\n", p, p2) var p3 *Person p3 = &p // p3 point to p and p3 save it address of p fmt.Printf("%+v %+v\n", p, *p3) p.Name = "John" // change p and p3 will be changed, beacuse reference to the save instance fmt.Printf("%+v %+v\n", p, *p3) fmt.Printf("%+v %+v %d %d\n", p, *p3, p3.Age, (*p3).Age) // in struct pointer will be dereferencing automatically updatePerson(&p, "Ted") fmt.Printf("%+v %+v %+v\n", p, p2, *p3) // p2 is copied, so will not be changed by p or *p3 update data } ``` ```go= // embedded struct import ( "encoding/json" "fmt" "github.com/golang-jwt/jwt" ) type Claims struct { Account string `json:"account"` jwt.StandardClaims } func main() { claims := Claims{ Account: "dachi", StandardClaims: jwt.StandardClaims{ // init for embedded struct Subject: "helloworld", Issuer: "project A", }, } result, _ := json.Marshal(claims) fmt.Println(string(result)) } ``` ### Struct & Receiver > - Value or Pointer > - value > - go 會使用 copy struct 方式傳入,因此拿到的是 struct 的複本 > - 用在讀取時,而不會對 struct 有寫入動作時 > - pointer > - go 會使用 copy struct address 方式傳入,因此拿到的是 struct 的地址,而透過 struct 的成員存取 `. (dot)` 會自動 dereferencing 到要修改的結構,不需要 `(*s).field` > - 可用在讀寫 > - 如果成員非常多的話,用 Pointer 傳入,可以剩下 Copy Value 更多的資源 > - 如果成員變數是 slice, map, channel, func, interface, pointer 的話,本身就是 address 了,所以透過 value 的傳遞進行修改的話,還是會改到本身變數去喔 > - 對於 slice 要特別小心,因為如果需要改變 len(), cap() 大小時,如 append 需要注意是否指向正確的 slice > - 就是說如果 receiver 是 value copy 而該 value 是指向真正資料的 address 時,直接對最終的資料做修改,完全無關 receiver 是 by value or by pointer 一樣都改的到最終的資料 > - 這邊的 value, pointer 指的是對 struct **本身** 的傳遞 ```go= type Cart struct { Name string // value type Price int // value type items []string // reference type (address) } func (c Cart) GetPrice() { fmt.Println("price:", c.Price) } func (c Cart) UpdatePrice(price int) { // c 為 struct copy 因此 c.Price 不是本來 struct 而是 copied 改變它不會影響本來的 struct c.Price = price } func (c Cart) InsertItem(itemName string) { // c 為 struct copy,但 items 是 reference type (address) 因此對它做操作仍然會改變該 items 的內容 fmt.Printf("%p\n", c.items) // 可以看到 address 是和建立的 items slice 完全一樣 for k, v := range(c.items) { // 找第一個不是空的空位塞入 if v == "" { c.items[k] = itemName // 直接改到目標 slice 受體的內容 break } } c.items = append(c.items, itemName) // 這個是無效的,因為 c 是 copied // 所以把 append 完新的 slice address 塞進去,只是丟給 copied struct 不會影響本來的 c struct } func (c Cart) ShowCart() { fmt.Println(c) } func main() { items := make([]string, 10) items[0] = "apple" fmt.Printf("%p\n", items) c := &Cart{ "bage", 100, items, // 把 slice address 塞過去 } c.ShowCart() c.UpdatePrice(200) c.InsertItem("cake") c.ShowCart() } // result // 0xc000072000 // {bage 100 [apple ]} // 0xc000072000 // {bage 100 [apple cake ]} ``` ### Struct & Interface > 1. 先定義 struct 可以考慮字首大小,或是field字首大小讓 package 外可以 ref 到 > 2. 再定義要開出來的 Interface 的功能 > 3. 實作建構 struct 初始的建構函數,回傳 Interface (這邊的 good practice 參考[ref](https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b)) > 4. 實作符合 Interface 要求的功能 ```go= // Point type IPoint interface { // public X() int Y() int SetX(int) SetY(int) Show() } type point struct { // private, invisable from the other package x int y int } func NewPoint(x int, y int) IPoint { // construct, return interface (interface is a pointer) p := &point{ x: x, y: y, } return p } func (p *point) Show() { // implement debug fmt.Println("X:", p.x, "Y:", p.y) } func (p *point) X() int { // implement get property return p.x } func (p *point) Y() int { // implement get property return p.y } func (p *point) SetX(x int) { // implement set property p.x = x } func (p *point) SetY(y int) { // implement set property p.y = y } // Point3D type IPoint3D interface { IPoint // extends interface Z() int SetZ(int) } type Point3D struct { // public, visable from the other package point// extends from the point struct z int } func NewPoint3D(x int, y int, z int) *Point3D { // return struct pointer p := &Point3D{} p.SetX(x) p.SetY(y) p.SetZ(z) // or /* p := &Point3D{ point: point{ x: x, y: y, }, z: z, } */ // or /* p := &Point3D{} p.x = x p.y = y p.z = z */ return p } func (p *Point3D) Z() int { return p.z } func (p *Point3D) SetZ(z int) { p.z = z } func (p *Point3D) Show() { // override show method fmt.Println("X:", p.point.X(), "Y:", p.point.Y(), "Z:", p.Z()) } func main() { point := NewPoint(2, 3) point.SetY(5) point.Show() fmt.Println(point.X()) point3D := NewPoint3D(1, 2, 3) point3D.z = 100 fmt.Println(point3D.X(), point3D.Y(), point3D.Z()) point3D.Show() } ``` ### Interface Assertion > 複雜的 interface variable 要一層一層 assertion 才能 range ```go= package main import "fmt" func main() { payload := map[string]interface{}{ "keys": []interface{}{ "hello", "world", }, "data": map[string]interface{}{ "foo": []struct { source string price int }{ { source: "chichen", price: 100, }, { source: "fish", price: 150, }, }, }, } keys := (payload["keys"]).([]interface{}) for k, v := range keys { fmt.Println(k, v.(string)) } data := (payload["data"]).(map[string]interface{}) for k, v := range data { newV := v.([]struct { source string price int }) for _, element := range newV { fmt.Println(k, element.source, element.price) } } } ``` > interface 什麼都能裝,但是要經過 assertion 才能轉為明確的型別,才可以用它的 method 不然只能用 interface 定出來的 method sets ```go= // member, member2 都實作了 show() // 只有 member 有 debug() // mSlice 可以放入所有實作 show() 的 obj // 但只有 assetion 回原本的 member 才能使用 debug() method type member struct { name string email string age int } func (m member) show() { fmt.Println("show:", m.name) } func (m member) debug() { fmt.Println("debug:", m) } type member2 string func (m member2) show() { fmt.Println("show:", m) } func main() { var mSlice []interface{ show() // 符合 interface 要求的 method sets } mSlice = append(mSlice, member{ name: "john", email: "john@sshellab.com", age: 35, }) mSlice = append(mSlice, &member{ name: "ted", email: "ted@sshellab.com", age: 25, }) mSlice = append(mSlice, member2("dachi")) mm := member2("geoge") mSlice = append(mSlice, &mm) // use .(type) to switch case the type for _, v := range mSlice { v.show() // 使用 interface 中定義的 show method switch v.(type) { case member: fmt.Println("is member:", v) member := v.(member) member.debug() // 經過 assertion 轉為 member 可使用 debug 的 method case *member: fmt.Println("it *member:", v) member := v.(*member) // 本來放進的就是指票,進行斷言也要使用指標型別 member.debug() case member2: fmt.Println("is member2:", v) case *member2: fmt.Println("is *member2:", v) } } // or use val, ok return 2 value to check assertion success if val, ok := mSlice[1].(*member); ok { // assertion success fmt.Printf("%v\n", val) } else { // assertion failed fmt.Println("is not type *member") } } ``` ### Func > Variadic Function 的不定參數,只能用在最後的 parameter ex: func foo(s string, num ...int) ```go= func add(a int, b int) (int, int) { d := a + b return d, a } func sum(s string, nums ...int) { // Variadic Functions fmt.Println(nums, s) total := 0 for _, v := range nums { total += v fmt.Printf("v:%d total:%d\n", v, total) } } func main() { m, n := add(1, 2) fmt.Printf("helloworld %d %d\n", m, n) main_nums := []int{1, 2, 3} sum("run1", main_nums[0]) sum("run2", main_nums[0], main_nums[2]) sum("run3", main_nums...) // pass slice to variadic functions and umpack with ... interface_slice := []interface{}{ // interface type slice to hold everying type "test1", 100, } fmt.Println(interface_slice) // unpack interface type slice } ``` ### Closure ```go= func intSeq() func() int { i := 0 return func() int { i++ return i } } func ain() { callback_func := intSeq() fmt.Printf("%d\n", callback_func()) } ``` ```go= // factory pattern // 透過不同的 TransferType 丟回一致的建構 function // 且呼叫之後得到 instance 都滿足了 interface Carrier func (fg *factoryGeneratorImpl) Gen(tp TransferType) Factory { switch tp { case AzureToAws: return func(srcKey, srcSecret, dstKey, dstSecret string) Carrier { return &azureAwsImpl{ srcAccount: srcKey, srcKey: srcSecret, dstAccount: dstKey, dstKey: dstSecret, } } case AzureToAzure: return func(srcKey, srcSecret, dstKey, dstSecret string) Carrier { return &azureImpl{ srcAccount: srcKey, srcKey: srcSecret, dstAccount: dstKey, dstKey: dstSecret, } } case AwsToAzure: return func(srcKey, srcSecret, dstKey, dstSecret string) Carrier { return &awsAzureImpl{ srcAccount: srcKey, srcKey: srcSecret, dstAccount: dstKey, dstKey: dstSecret, } } default: return nil } } ``` ### Iterates (Range) ```go= nums := []int{ 2, 3, 4, } for i, num := range nums { // for slice, array return index, element fmt.Println(i, num) fmt.Printf("%T, %T\n", i, num) } for _, num := range nums { // ignore the first return index fmt.Println(num) } kvs := map[string]string{ "a": "apple", "b": "banana", } for k, v := range kvs { // for map return key, value fmt.Printf("%s -> %s\n", k, v) } for _, v := range kvs { // only use the value fmt.Println(v) } s := "hello world" for i, char := range s { // for string return index, charactor ASCII fmt.Printf("%T, %T, %s\n", i, char, string(char)) } student_rank_list_map := []map[string]int{ { "ken": 1, "ted": 2, }, } student_rank_list_map = append(student_rank_list_map, map[string]int{ "john": 3, }) for i, rank_map := range student_rank_list_map { for k, v := range rank_map { fmt.Printf("%d, %s, %d\n", i, k, v) } } ``` ### Error ```go= type error interface { Error() string } ``` > 其實 error 是個 interface 只要滿足 method sets 實作了 Error() string 都可以是 error > > 當 error 用在 fmt 將有以下特性 > > >If an operand implements the error interface, the Error method will be invoked to convert the object to a string > > error 是個 interface 其中包含了 **(Type, Value)** 兩者都是 nil 才會是真的 nil > ```go= > func do() error { > var err *someError > return err // nil of type *someError > } > > if err := do(); err == nil { > // always not equal to nil (*someError, nil) > } > ``` > 所以為了讓外面可以 **if err != nil {}** 那就要讓拋回 error interface 的 func 總是以 nil 傳回,不要 declare 是那種型別的 error,不然外面會有雷 ```go= func runReturnErr() error { return fmt.Errorf("create an error obj") } type myErr struct { msg string } func (e myErr) Error() string { return fmt.Sprintf("my error: %s", e.msg) } func (e myErr) Detail() { fmt.Printf("detail of error: %s\n", e.msg) } func NewMyErr(errStr string) error { return myErr{ msg: errStr, } } func main() { // type error interface { Error() string } err := runReturnErr() if err != nil { // 以下全部一樣 fmt.Printf("%s\n", err.Error()) // 呼叫 .Error() 可取得 err 內容 fmt.Printf("%s\n", err) // 呼叫 .Error() 可取得 err 內容 fmt.Println(err) // 直接印 err 可以呼叫 String() } // use custom error customErr := NewMyErr("you get an error!!!") fmt.Println(customErr) // assertion to myErr to use Detail() method if assertedMyErr, ok := customErr.(myErr); ok { assertedMyErr.Detail() // 轉型完使用 myErr 專有的 Detail() } var theErr = myErr{ msg: "you get an error!!!", } if errors.Is(customErr, theErr) { // compare two error is the same fmt.Println("yes is myErr") } else { fmt.Println("no is not myErr") } } ``` ```go= // define a project's error package error type DaoErrorType int const ( DB_ACCESS_FAILED DaoErrorType = iota RESOURCE_DUPLICATE ) type DaoError struct { detail string errType DaoErrorType } func NewDaoError(errType DaoErrorType, detail string) error { return DaoError{ detail: detail, errType: errType, } } func (e DaoError) Error() string { return e.detail } func (e DaoError) Type() DaoErrorType { return e.errType } /* use DaoError daoErr, _ := err.(evergreen_error.DaoError) switch daoErr.Type() { case evergreen_error.DB_ACCESS_FAILED: RFC7807ErrorResponse(ctx, http.StatusInternalServerError, "create asset video failed", daoErr, "") case evergreen_error.RESOURCE_DUPLICATE: RFC7807ErrorResponse(ctx, http.StatusBadRequest, "create asset video failed", daoErr, "") } */ ``` ### Defer > - 先宣告後執行 > - func call 最深層先執行到最淺層,且在離開 func 前執行 > - 當出現 defer 時需要執行的 func 中的值已經先 evaluate 完 ```go= func calc(index string, a, b int) int { ret := a + b fmt.Println(index, a, b, ret) return ret } func main() { a := 1 b := 2 // 先宣告後執行 defer calc("1", a, calc("10", 2, b)) // 10, 2, 2, return 4 先 evaluate 與 a = 1 先帶入了 ,再掛上 defer 最後執行 a = 0 defer calc("2", a, calc("20", a, b)) // 20, 0, 2, return 2 先 evaluate 與 a = 0 帶入,再掛上 defer 等後先執行 b = 1 // 執行 calc("2", 0, 2) => 2 result: 2, 0, 2, 2 // 執行 calc("1", 1, 4) => 5 result: 1, 1, 4, 5 } /* 10 2 2 4 20 0 2 2 2 0 2 2 1 1 4 5 */ ``` ### Panic > 遇到運作時無法處理的狀況,就是 panic 直接結束離開,一般用在條件不符合程式運作的基本需求下 ```go= import ( "os" ) func main() { _, err := os.Stat("/test") // 檔案必須存在,否則不能運作 if err != nil { panic("the file should exist") } } ``` ## OOP > 透過 struct embedded 和 interface 來實現 ```go= package main import "fmt" type Car struct { name string } func (c *Car) Start() { fmt.Println(c.name, "engine start") } func (c *Car) Stop() { fmt.Println(c.name, "engine stop") } type VW struct { Car } func NewVW(name string) *VW { return &VW{ Car: Car{ name: name, }, } } func (v *VW) Start() { fmt.Println("VW", v.name, "engine start") } type Toyata struct { Car } func NewToyata(name string) *Toyata { return &Toyata{ Car: Car{ name: name, }, } } func (t *Toyata) Stop() { fmt.Println("toyata", t.name, "engine stop") } type CarRun interface { Start() Stop() } func show(c CarRun) { c.Start() c.Stop() } func main() { car1 := NewVW("sportsvan") show(car1) car2 := NewToyata("camery") show(car2) } ``` ## Go Multiple version (before go1.21) [Managing Go installations](https://go.dev/doc/manage-install) (deprecated) > Go 可以讓你在 local 裝多個版本,但必須是先從標準 download page 先裝一個 **Global** 版本,再由這個版本的 go 來安裝其它的版本 - go1.21.4 (/usr/local/go) **Go 官方安裝標準目錄** - go1.22.4 (~/sdk/go1.22.4) **由 go1.21.4 來安裝的 1.22.4** - go1.22.0 (~/sdk/go1.22.0) **由 go1.21.4 來安裝的 1.22.0** - ... ```bash= # install go 1.22.4 go install golang.org/dl/go1.22.4@latest go1.22.4 download go1.22.4 env GOROOT ``` ```bash= # uninstall go 1.22.4 rm -rf ~/go/bin/go1.22.4 rm -rf ~/sdk/go1.22.4 ``` > Go 1.21 之後**將 go 版本和 go toolchain 版本作為一個 module 的依賴來管理**,這表示當在 go.mod 中定義了 go version 則 local go version 只要在 1.21 以後的版本,都可以自動依 go.mod version 進行 go version, toolchain 的下載 > 而在 go.mod version 中的版號變成**最小滿足條件** - 情境1 - local toolchain: 1.21.4 - go.mod version 1.22.4 這種情況 go toolchain 本身設定 auto 會自動選擇,因此會自動安裝 toolchain 1.22.4 (~/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.22.4.darwin-arm64/) 來滿足編譯 但是如果透過修改 **GOTOOLCHAIN=local** 表示使用 local (/usr/local/go) 安裝的 1.21.4 來編譯就會出現 ``` go: go.mod requires go >= 1.22.4 (running go 1.21.4; GOTOOLCHAIN=local) ``` - 情境2 - local toolchain: 1.21.4 - go.mod version 1.22.4 - GOTOOLCHAIN=go1.22.2 ``` go: go.mod requires go >= 1.22.4 (running go 1.22.2; GOTOOLCHAIN=go1.22.2) ``` ### Go project version control > 從 1.21 開始前後兼容做了很大的改變 go.mod 中的 go version 為**編譯語言最小可接受版本**,而 toolchain 版本為實際編譯可用的工具版本,因為保證向下相容的關係,go toolchain 會盡可能選擇最新的 toolchain,如果是設定 **GOTOOLCHAIN=auto** 表示它用了 local+auto 如果要求的 toolchain 比 local 大,就會自動安裝,否則使用 local (保證向下相容) > Go 現況來說從 1.21 之後自動處理 go version 和 toolchain 的相依問題,因此只需要 go mod init 之後修改 go.mod version 它就會自動下載 toolchain,所以在 go project 目錄下,所有的 go version 都會跟著 module 中定義的 version 進行切換 [go1.21 toolchain](https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/) ``` go get go@1.22.4 go get toolchain@go1.22.4 ``` - Project upgrade - go get go@1.22.4 (自動下載 toolchain 並使用新的 toolchain) - Project downgrade - go get go@1.22.3 - go get toolchain@go1.22.3 (手動指定,同步 go version) ![image](https://hackmd.io/_uploads/Bkg5AQYSR.png) ### Check Go module version > go version -m gopls (不知道該 binary 是那個 go version build 出來時,可以這樣確認) ![image](https://hackmd.io/_uploads/SJXUDOZJkl.png) ## Go Module >> go env | grep GO111MODULE > > 存在一個神奇的環境變數,它在 1.11 開始出現 1.12 和 1.11 行為一樣,但是 1.13 之後又改變了,而 1.16 預設它是開啟 > > - ver 1.11, 1.12 > - GO111MODULE=auto (就是看有沒有在 GOPATH 內) > - 在 GOPATH 外自動設為 GO111MODULE=on > - 在 GOPATH 內,就算有 go.mod 還是設為 GO111MODULE=off > - 所以在 GOPATH 內,想用 Go Module 就要 > - GO111MODULE=on go get github.com/xx/xxx@v1.3.1 > - ver 1.13+ > - GO111MODULE=auto (看 GOPATH 外內且有沒有 go.mod) > - 在 GOPATH 外且有 go.mod 就是 GO111MODULE=on > - 在 GOPATH 內且沒有 go.mod 就是 GO111MODULE=off > - ver 1.16 > - 預設就是使用 Go module > - 可以自己設為 GO111MODULE=auto (就看有沒有 go.mod 有就是 on) > import package 時 Go 會先在 $GOROOT/src 下找,找不到,就會到 go.mod 定義中的位址去找 (GO111MODULE=on) 這樣就解除了 $GOPATH 的相依 > >> go env -w GOPRIVATE="gitlab.kkinternal.com/*" >> 可以告訴 Go 跳過 proxy 直接抓 private repo >> go env -u GOENV 當設錯要回復到 default 可以用 -u >> GOPRIVATE go 和 **go get**, **go mod tidy** 有關,這個時預設會去 goproxy 拿,所以沒指定 private repo path 的時候,就會找不到,這個和 .netrc 沒關,而 netrc 是訪問的權限。如果 go.mod go.sum 都 commit 的話 (已過 go mod tidy) 那其實不需要 GOPRIVATE 這個 env 了,因為 go mod download 會對 go.sum 照單全收,所以只剩下訪問權限要處理而己 > 因為 Go ENV 是全域的,就是當改變了會影響所有 go 的環境,所以只過只是在該 console 要測試的話,直接使用 export GOENV=XXX 來暫時覆蓋就好,就算設錯了,直接關掉重開就好了,確定之後在寫入 Go ENV 中 > - [Using go Modules](https://blog.golang.org/using-go-modules) > - go get 下載並加入一筆 go.mod 的 require 相依 > - go get gitlab.com/dachichang/gotestmod@latest (最新的 tag) > - go get gitlab.com/dachichang/gotestmod@main (main branch 最新 commit) > - go get gitlab.com/dachichang/gotestmod@v1.0.0 (指定某個版本) > - go get gitlab.com/dachichang/gotestmod/v2 (抓第 v3) > - go mod tidy 修改 go.mod 後,修正相依,以及刪除用不到的依賴 > - 當改 require v1.0.1 -> v1.1.0 時,做 tidy 會自動下載相依,移除之前的相依 > - 等同於做了 go mod download > - go get -u ./... && go mod tidy > - 升級所有相依套件,並整理相依 > - go mod init > - 開一個 project 先做 go mod init ${PROJECT_NAME} 就可以視為整個目錄都是使用 Go Module > - 也等於 GO111MODULE=on 被設置了 > - go list -u -m all > - 列出所有使用的 modules > - go list -m -versions gitlab.com/dachichang/gotestmod > - 列出該 module 所有的版本 > - go mod why all > - 列出所有 packages 為什麼被相依 > - go mod why -m all > - 列出所有 modules 為什麼為相依 > - go.mod > - module 定義模組位址和模組名稱 {MODULE_ADDR}/{MODULE_NAME} > - require 依賴的 module 的版本 > - exclude 排除某個 module 的版本 > - replace 使用不同的 module 替換本來 require 的版本 > - 通常用 replace 來換掉本地開發時的相依很方便 > - ex: replace gitlab.com/dachichang/gotestmod => ../gotestmod > - indirect 被間接依賴的 module 由 go get 或 go tidy 自動帶入 ```bash= go get gitlab.com/dachichang/gotestmod // 安裝最新版本(tags) go get gitlab.com/dachichang/gotestmod@main // 安裝 main branch go get gitlab.com/dachichang/gotestmod@v1.1.4 // 指定版本安裝 go list -m -versions gitlab.com/dachichang/gotestmod // 列出所有的版本 go get -u gitlab.com/dachichang/gotestmod // 升級到最新的版本,但是不會跳 major version go get gitlab.com/dachichang/gotestmod@v1.1.3 // 退到指定版本 ``` 另外一種方式直接 vim go.mod 直接改裡面的版本再透過 go mod tidy 修正相依 ```bash= // go.mod module a.com/gotestmaingg go 1.16 require gitlab.com/dachichang/gotestmod v1.1.4 (改成 v1.1.3) replace gitlab.com/dachichang/gotestmod => ../gotestmod ``` ```bash= go mod tidy // 修正相依,順便可以把之前 go get 的歷史記錄舊參考清掉 ``` ### 如何使用寫好的 module ```go= // create main module mkdir gotestmain cd gotestmain go mod init gotestmain go get gitlab.com/dachichang/gotestmod // 下載相依 module . ├── go.mod // 自動被建立,做完 go get 後會被新增一筆 require ├── go.sum // 記錄了 module 相依的歷史 ├── lib.go // 部份的 main package 的公用函式 └── main.go // 主程式進入點,必須為 main package & func main() ``` ```go= // lib.go package main func echo(s string) string { return s } ``` ```go= // main.go package main import ( "fmt" "gitlab.com/dachichang/gotestmod/foo" // import 指定 package 的 module 位址 "gitlab.com/dachichang/gotestmod/foo/bar" // 對像是 package 所以想有那個 package 都要 import 它的絕對位址 "gitlab.com/dachichang/gotestmod/space" // ${MODULE_PATH}/${MODULE_NAME}/${PACKAGE_DIR} ) func main() { fmt.Println(echo("hello, world")) // echo 在 lib.go 同 main package 直接使用 myfoo.ShowFoo() // 在 foo 目錄下的 myfoo package, 可以不同名,記得以 package name 為主不是目錄名稱,目錄名稱只是 import 時的位址 fmt.Println(bar.ReturnShowBar()) // bar package 剛好和目錄名稱一樣 point := space.NewPoint(1, 2) point.SetX(5) fmt.Println(point) bar.ShowPrivateBar() } ``` > go get 和 go mod download/tidy 行為不同 > > - go get 看 remote 版本進行安裝,或是升級,因此需要 `git ls-remote -q origin` 來看 repo 的 go.mod 的資訊 > - 套件升級直接透過 go get -u all > - 但遇到 private repo 時, **必須** 使用 `~/.netrc` 來解,因為它需要遞迴的進 subgroup 去看 go.mod > ```bash= > machine privategitlab.company.com login USERNAME_HERE password TOKEN_HERE > ``` > - 就可以開心的 `go get -u all` > > - go mod download or tidy 由已知的 go.mod 進行下載或是移除不需要的相依性 > - 套件升級自己編輯 go.mod vX.X.X 並透過 tidy 進行下載和整理 > - 無法對所有相依的 go module 進行 patch upgrade > - 如果下載了已經帶有 go.mod 且用到 private repo 而要下載相依的 go module,要透過以下的方法 > ```bash= > git config \ > --global \ > url."https://USER:USER_TOKEN@privategitlab.com".insteadOf \ > "https://privategitlab.com" > ``` > - 就可以開心的 `go mod download` > - 參考 > - [Common-Mistakes](https://gist.github.com/MicahParks/1ba2b19c39d1e5fccc3e892837b10e21#common-mistakes) ### 使用 local module > 當 main module 相依到自己推的其它 module 需要測試時,為確保 libary 的改動和 main module 的改動都正確,可以使用 **replace** 的定義把在 go.mod 中的 require 定義成 local module reference ```bash= 目錄結構如下 . ├── math (module shellab.io/math) <- library │ ├── go.mod │ └── math.go └── school (module school) <- main ├── go.mod └── main.go ``` school/go.md ```go= module school go 1.18 require shellab.io/math v1.0.0 replace shellab.io/math => ../math ``` math/go.md ```go= module shellab.io/math go 1.18 ``` school/main.go ```go= package main import ( "fmt" "shellab.io/math" // 這將會使用到 local module 因為在 go.mod 使用 replace 取代為 local module ) func main() { fmt.Println(math.Add(2,4)) } ``` ### Major version of library > - go module path 定義 version > - 當更新 module library 版本時透過 module path 定義 ex: module shellab.io/gotestmod/v2 配合打上合適的 tag > - 以 Semantic Versioning 來看 **v{major_version}.{minor_version}.{patch_version}** golang 在更新時只會動 minor_version, patch_version 而 major_version 視為不同的 library > - 使用 **go get shellab.io/gotestmod/v2** 來裝 > - 在實作一個 module 時可以使用相同 libary 的不同版本,為了必免 conflict 只要給它個 alias 就可以解決了 > - ```go= > import ( > "shellab.io/gotestmod" > gotestmod_v2 "shellab.io/gotestmod/v2" > ) > ``` > - go module path 不定義 version 靠上 tag > - 不在 module path 中定義不同版本,只打上不同的 tag ,則裝完之後出現 **+incompatible** 告知目前版本不會向下相容,你的程式可能會爛掉 > - ex: github.com/natefinch/lumberjack v2.0.0+incompatible > - 直接透過 go get @version 來裝 > - 使用 **go get shellab.io/gotstmod@v2.1.2** 來裝 > - [version control 的機制](https://mileslin.github.io/2020/08/Golang/%E5%88%B0%E5%BA%95-go-get-%E7%9A%84%E7%89%88%E8%99%9F%E6%80%8E%E9%BA%BC%E9%81%8B%E4%BD%9C%E7%9A%84/) ```bash= > mkdir go_major_version && cd go_major_version > go mod init go_major_version > go get -u gitlab.com/shellab/golang/gotestmod > go get -u gitlab.com/shellab/golang/gotestmod/v2 ``` go.mod ```go= module goversion go 1.18 require ( gitlab.com/shellab/golang/gotestmod v1.2.1 gitlab.com/shellab/golang/gotestmod/v2 v2.0.1 ) ``` main.go ```go= // main module 使用同個 package 的不同版本 package main import ( "gitlab.com/shellab/golang/gotestmod/foo" v2_myfoo "gitlab.com/shellab/golang/gotestmod/v2/foo" // 避免 conflic 給 alias ) func main() { myfoo.ShowFoo() v2_myfoo.ShowFoo() } ``` ### 使用 go tool (1.24+) 來管工具相依性 - `go get -tool import_path@version` - ex: go get -tool github.com/swaggo/swag/cmd/swag@v1.16.5 - ex: go get -tool aqwari.net/xml/cmd/xsdgen - `go list tool` 來顯示裝那些相依套件 ![image](https://hackmd.io/_uploads/SkXEN2vqeg.png) - `go tool xsdgen -version` 執行 xsdgen cmd - `go tool -n xsdgen` 印出該 cmd 存在什麼地方 #### 刪除工具相依 > 直接到 go.mod 把 tool () 中不需要的刪除,再做 ==go mod tidy== ## Unit test ``` // unit test layout . ├── car │ ├── car.go │ └── car_test.go ├── go.mod ├── go.sum └── main.go ``` > - 將測試放在同個 package 下,以 **_test.go** 命名檔案 > - ex: upload_file_validation_handler_test.go > - 所有測試案例都以 **Test** 為 Prefix func name > - ex: TestFindMimetypeMedia ### Go test cmdline > - go test -cover -v ./... > - 整個Module 中所有的 test 包含算出 coverage 還有測試細節結果 > - go test -cover -v ./pkg/service/... > - Module 中的某個 service package > - go test -coverprofile=prof.out ./... > - 產生 coverage profile 可以用來轉換成 coverage report > - go tool cover -html=prof.out -o report.html > - 透過 coverage profile 產生 coverage report ```go= package handler import ( "testing" "github.com/gabriel-vasile/mimetype" "evergreen-workflow/pkg/imagemagick" "evergreen-workflow/pkg/mediainfo" ) func TestFindMimetypeMedia(t *testing.T) { videoPath := "../../test/data/sample_video.mp4" video, _ := mediainfo.NewMediainfoFromFile(videoPath) videoMimeType, _ := mimetype.DetectFile(videoPath) type result struct { mimetype string err error } // cases provider var tests = []struct { userDefinedType string mediainfo *mediainfo.Media mimetype *mimetype.MIME result result }{ { userDefinedType: "video", mediainfo: video, mimetype: videoMimeType, result: result{ mimetype: "video/mp4", err: nil, }, }, } // for all the cases test result for _, test := range tests { result, err := findMimetypeMedia(test.userDefinedType, test.mimetype.String(), test.mediainfo) if result != test.result.mimetype || err != test.result.err { t.Error("parse error") } } } func TestSha1Hash(t *testing.T) { sha1, _ := sha1Hash("../../test/data/sample_video.mp4") if sha1 != "1385f0808b7ef1600b6c39244917ef1df2ce6652" { t.Error("sha1 hash is incorrect") } } func TestDecodeKeyPath(t *testing.T) { _, _, _, err := decodeKeyPath("video-xxxxxxx.mp4") if err == nil { t.Error("key path should parse error") } } func TestCheckImageDimension(t *testing.T) { image, _ := imagemagick.NewImageMagickFromFile("../../test/data/sample_graphic.webp") result := checkImageDimension(image) if result == true { t.Error("image is not fit to the spec, should return false") } } func TestCheckImageAspectRatio(t *testing.T) { image, _ := imagemagick.NewImageMagickFromFile("../../test/data/sample_graphic.webp") result := checkImageAspectRatio("landscape", image) if result == true { t.Error("image is not match landscape spec, should return false") } } ``` ## Design Package > - package 分兩種 > - main package > - 為應用程式執行的 entry point > - 只能有一個 > - 沒有 main package 無法編譯成可執行的檔案 > - go build > - 會依 go.mod 中 module name 編出「執行檔名」 > - ex: gitlab.com/dachichang/gotestmain => gotestmain (執行檔名) > - go install > - 裝安到 $GOPATH/bin 下面 > - library package > - 不可以執行,但是可以給可執行的 main package 做使用 > - 要注意 > - 目錄下同級檔案屬於同一個 package > - package name 和目錄名稱可以不一樣,但是正常來說應該要設一樣 > - 大寫開頭:在 package 之外可見,稱作 exported > - 小寫開頭:僅限於 package 內使用,稱作 unexported > package layout (gotestmod) > - go.mod (gitlab.com/dachichang/gotestmod) > - foo/ (import path name gitlab.com/dachichang/gotestmod/foo) > - bar/ (import path name gitlab.com/dachichang/gotestmod/foo/bar) > - b.go (檔名不重要 pakcage bar 其它使用該 pakcage 要用 bar.xxx) > - c.go (同為 pakcage bar,可以直接用 b.go 中的定義) > - a.go (檔名不重要, package myfoo 其它使用該 package 要用 myfoo.xxx, 不是 foo. import 只是定位「地址」而使用 package 要依 package 的定義,通常會定義成一樣的) > - space/ (import path name gitlab.com/dachichang/gotestmod/space) > - point.go (檔名不重要, package space 其它使用該 package 要用 space.xxx) > - point_3d.go [範例在這裡](https://gitlab.com/dachichang/gotestmod) ```bash= # create a module mkdir gotestmod cd gotestmod # 目錄名稱不重要,但是應該要和 mod name (gotestmod) 一樣 go mod init gitlab.com/dachichang/gotestmod #${MODULE_ADDR}/{MODULE_NAME} 這個 gotestmod 為 build 出來的名稱 . ├── README.md ├── foo │ ├── a.go │ └── bar │ ├── b.go │ └── c.go ├── go.mod └── space ├── point.go └── point_3d.go ``` ```go= // go.mod module gitlab.com/dachichang/gotestmod go 1.1 ``` ```go= // foo/a.go package myfoo import ( "fmt" "gitlab.com/dachichang/gotestmod/foo/bar" ) func ShowFoo() { fmt.Println("pakcage myfoo, func ShowFoo !!!") fmt.Println("call bar.ReturnShowBar", bar.ReturnShowBar()) } ``` ```go= // foo/bar/b.go package bar func ReturnShowBar() string { return "package bar, func ReturnShowBar and return string" } func showBar() string { return "bar private function!!" } ``` ```go= // foo/bar/c.go package bar import ( "fmt" ) func ShowPrivateBar() { fmt.Println("hello, world", showBar()) } ``` ```go= // foo/space/point.go package space import ( "fmt" ) type IPoint interface { // public X() int Y() int SetX(int) SetY(int) } type point struct { // private, invisable from the other package x int y int } func NewPoint(x int, y int) IPoint { // construct, return interface (interface is a pointer) p := &point{ x: x, y: y, } return p } func (p *point) String() string { // implement debug return fmt.Sprintf("x: %d y: %d", p.x, p.y) } func (p *point) X() int { // implement get property return p.x } func (p *point) Y() int { // implement get property return p.y } func (p *point) SetX(x int) { // implement set property p.x = x } func (p *point) SetY(y int) { // implement set property p.y = y } ``` ```go= // foo/space/point_3d.go package space import ( "fmt" ) // Point3D type IPoint3D interface { IPoint // extends interface Z() int SetZ(int) Show() } type Point3D struct { // public, visable from the other package point// extends from the point struct z int } func NewPoint3D(x int, y int, z int) *Point3D { // return struct pointer p := &Point3D{} p.SetX(x) p.SetY(y) p.SetZ(z) // or /* p := &Point3D{ point: point{ x: x, y: y, }, z: z, } */ // or /* p := &Point3D{} p.x = x p.y = y p.z = z */ return p } func (p *Point3D) Z() int { return p.z } func (p *Point3D) SetZ(z int) { p.z = z } func (p *Point3D) Show() { // override show method fmt.Println("X:", p.point.X(), "Y:", p.point.Y(), "Z:", p.Z()) } func (p *Point3D) String() string { return fmt.Sprintf("x: %d y: %d z: %d", p.x, p.y, p.z) } ``` ## IO package > - Golang 對 IO 讀寫提供很多的 package 各有目的 > - io > - 定義一些基本接口 interface 和 const > - 像是常見的 Reader, Writer interface > - io.EOF const > - ioutil > - io 的工具包,比較實用的功能 > - ReadAll, ReadFile, WriteFile, ReadDir > - 它的實作可以看到它都是呼叫 io, os 等 package 做一些整合工具 > - bufio > - 在 io package 上面多封裝一層,加了緩存功能,常和 ioutil, bytes.Buffer 搞混 > - 它和 ioutil 差異在於**緩存**在讀大檔案時候,可以一段一段的讀到 memory 而 ioutil 讀完可能會 OOM > - 它和 bytes.Buffer 差異在於,bufio 用在**文件**到**記憶體**,而不是**記憶體**到**記憶體** > - bytes, strings > - 兩個差不多都且實現了 Reader ,只是操作對象不同一個是 string 一個是 bytes ### IO interface ```go= // defined for io read, write interface type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } ``` ### strings.Reader ```go= // strings.Reader Read implements the io.Reader interface. type Reader struct { s string i int64 // current reading index prevRune int // index of previous rune; or < 0 } func NewReader(s string) *Reader { return &Reader{s, 0, -1} } func (r *Reader) Read(b []byte) (n int, err error) { if r.i >= int64(len(r.s)) { return 0, io.EOF } r.prevRune = -1 n = copy(b, r.s[r.i:]) r.i += int64(n) return } ``` ### io/ioutil package (deprecated by os package) > **因為是一次性讀寫,所以文件的內容不能過大,方便用於小文件的一次讀入和寫出** > **未來將棄用所有功能在 os package 都可以找到** - file - ioutil.ReadAll -> io.ReadAll - ioutil.ReadFile -> os.ReadFile - ioutil.ReadDir -> os.ReadDir - ioutil.WriteFile -> os.WriteFile - others - ioutil.NopCloser -> io.NopCloser - ioutil.ReadDir -> os.ReadDir - ioutil.TempDir -> os.MkdirTemp - ioutil.TempFile -> os.CreateTemp ```go= // 都是 wrapper function func ReadAll(r io.Reader) ([]byte, error) { return io.ReadAll(r) } func ReadFile(filename string) ([]byte, error) { return os.ReadFile(filename) } func WriteFile(filename string, data []byte, perm fs.FileMode) error { return os.WriteFile(filename, data, perm) } func ReadDir(dirname string) ([]fs.FileInfo, error) { f, err := os.Open(dirname) if err != nil { return nil, err } list, err := f.Readdir(-1) f.Close() if err != nil { return nil, err } sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() }) return list, nil } ``` ### bufio package > - Reader 和 Scanner 差不多可以替代使用 > - Reader > - 讀到長度 4k > - Scanner > - 可讀到長度 64k > - 具有**斷詞**功能,給定 token pattern 可以自動分段讀取 ```go= // bufio.Reader func main() { file, err := os.Open("./testFile.txt") if err != nil { fmt.Println(err) os.Exit(1) } defer file.Close() // file 有實作 Read() 功能 io.Reader interface reader := bufio.NewReader(file) for { line, err := reader.ReadString('!') // 讀入的 line 有包含了 "!" 不需要的話要自已排除 if err != nil { if err == io.EOF { break } else { fmt.Println(err) os.Exit(1) } } fmt.Print(line) } } ``` ```go= // bufil.Scanner // The simplest use of a Scanner, to read standard input as a set of lines. func ExampleScanner_lines() { scanner := bufio.NewScanner(os.Stdin) // 預設的 splitFunc 是 bufio.ScanLines for scanner.Scan() { // 讀入不包含換行, EOF 或是 Error 時停下來 fmt.Println(scanner.Text()) // Println will add back the final '\n' } if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "reading standard input:", err) } } // Use a Scanner to implement a simple word-count utility by scanning the // input as a sequence of space-delimited tokens. func ExampleScanner_words() { // An artificial input source. const input = "Now is the winter of our discontent,\nMade glorious summer by this sun of York.\n" scanner := bufio.NewScanner(strings.NewReader(input)) // Set the split function for the scanning operation. scanner.Split(bufio.ScanWords) // Count the words. count := 0 for scanner.Scan() { //掃到錯誤停下來,或是到 EOF count++ } if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "reading input:", err) } fmt.Printf("%d\n", count) // Output: 15 } // Use a Scanner with a custom split function (built by wrapping ScanWords) to validate // 32-bit decimal input. func ExampleScanner_custom() { // An artificial input source. const input = "1234 5678 1234567901234567890" scanner := bufio.NewScanner(strings.NewReader(input)) // Create a custom split function by wrapping the existing ScanWords function. split := func(data []byte, atEOF bool) (advance int, token []byte, err error) { advance, token, err = bufio.ScanWords(data, atEOF) if err == nil && token != nil { _, err = strconv.ParseInt(string(token), 10, 32) } return } // Set the split function for the scanning operation. scanner.Split(split) // Validate the input for scanner.Scan() { fmt.Printf("%s\n", scanner.Text()) } if err := scanner.Err(); err != nil { fmt.Printf("Invalid input: %s", err) } // Output: // 1234 // 5678 // Invalid input: strconv.ParseInt: parsing "1234567901234567890": value out of range } ``` ## Useful Algorithm ### Sort - [simple quick sort video](https://www.youtube.com/watch?v=kFeXwkgnQ9U) ```go= // quick sort package main import "fmt" func main() { data := []int{10, 5, 25, 351, 14, 9} fmt.Printf("%#v", quickSort(data)) } func quickSort(data []int) []int { if len(data) <= 1 { return data } lastIdx := len(data) - 1 pivot := data[lastIdx] // the last one data = data[:lastIdx] itemGreater := []int{} itemLower := []int{} for _, v := range data { if v > pivot { itemGreater = append(itemGreater, v) } else { itemLower = append(itemLower, v) } } lowerResult := quickSort(itemLower) greaterResult := quickSort(itemGreater) result := append(lowerResult, pivot) result = append(result, greaterResult...) return result } ``` ### Collection Functions ```go= // 找出 slice 中符合 target 在那個欄位 func index(data []string, target string) int { for i, v := range data { if v == target { return i } } return -1 } // slice 包含是否資料,或是是否存在資料 func include(data []string, target string) bool { return index(data, target) >= 0 } // 透過 callback func 來 filter out 要的資料 func filter(data []string, f func(string) bool) []string { var resultData []string for _, v := range data { if f(v) { resultData = append(resultData, v) } } return resultData } // 將 slice 中每個資料都套用 func 並存到新的 slice 去 func dataMap(data []string, f func(string) string) []string { resultData := make([]string, len(data)) // 每個都要做,已知輸出的 slice 大小用 make 先做出來 // 用 make 做出 cap ,會比 append 效率好 for i, v := range data { resultData[i] = f(v) } return resultData } func main() { strs := []string{"peach", "apple", "pear", "plum"} fmt.Println(index(strs, "pear")) fmt.Println(include(strs, "plum")) fmt.Println(filter(strs, func(s string) bool { return len(s) > 4 })) fmt.Println(filter(strs, func(s string) bool { return strings.Contains(s, "u") })) fmt.Println(dataMap(strs, func(s string) string { return strings.ToUpper(s) })) } ``` ## Design Pattern ### Interface Polymorphism ```go= package storage import ( "context" "io" "cloud.google.com/go/storage" ) // storage interface type Storage interface { Upload(content io.ReadCloser, path string) error Download(path string) (io.ReadCloser, error) } // implementation struct type GoogleStorageImpl struct { client *storage.Client Bucket string } // Constructor for the implementation struct func NewGoogleStorage(bucket string) (*GoogleStorageImpl, error) { gcsClient, err := storage.NewClient(context.Background()) if err != nil { return nil, err } return &GoogleStorageImpl{ client: gcsClient, Bucket: bucket, }, nil } // implement interface Upload func (g *GoogleStorageImpl) Upload(content io.ReadCloser, path string) error { defer content.Close() uploadObj := g.client.Bucket(g.Bucket).Object(path) w := uploadObj.NewWriter(context.TODO()) defer w.Close() _, err := io.Copy(w, content) if err != nil { return err } return nil } // implement interface Download func (g *GoogleStorageImpl) Download(path string) (io.ReadCloser, error) { reader, err := g.client.Bucket(g.Bucket).Object(path).NewReader(context.TODO()) if err != nil { return nil, err } return reader, nil } ``` ### Functional Options ```go= type Server struct { host string port int secure bool timeout time.Duration } // 每個 option 都是處理 Serve struct 的一種設定 type Option func(*Server) // 當 create object 時可以給 1~N 個 Option func NewServer(opts ...Option) *Server { // 預設 config srv := &Server{ host: "localhost", port: 8080, timeout: 30 * time.Second, } // 依每個 Option 套上 for _, opt := range opts { opt(srv) } return srv } // WithHost configures the Server's host func WithHost(host string) Option { return func(s *Server) { // 可以再做一些處理 s.host = host } } // WithPort configures the Server's port func WithPort(port int) Option { return func(s *Server) { // 可以再做一些處理 s.port = port } } // Usage srv := NewServer( WithHost("example.com"), WithPort(9090), ) ``` ### Singleton ```go= package database import ( "fmt" "os" "sync" log "github.com/sirupsen/logrus" "gorm.io/driver/postgres" "gorm.io/gorm" "evergreen-backend/pkg/environment" ) var once sync.Once var singletonDB *gorm.DB func GetDB() *gorm.DB { once.Do(func() { dsn := fmt.Sprintf("host=%s user=%s password=%s port=%d dbname=%s", os.Getenv("DB_HOST"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWD"), 5432, os.Getenv("DB_NAME"), ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { log.Fatalf("Can't connect to database with DSN: %s", dsn) } runtimeEnv := environment.GetEnvironment(os.Getenv("ENV")) if runtimeEnv == environment.DEV { // when in dev environment default enable db debug mode singletonDB = db.Debug() } else { singletonDB = db } }) return singletonDB } func MigrateVersion() uint { db := GetDB() migrateInfo := struct { Version uint }{} db.Table("schema_migrations").Select("version").First(&migrateInfo) return migrateInfo.Version } ``` ## Tools ### gofmt > 幫忙格式化內容 - gofmt routers/router.go | diff -u routers/router.go - 比較看看格式改了什麼 - gofmt -w example.go - 直接正規化蓋掉 - gofmt -w . - 對以下整個目標做正規化蓋掉 ## Example - [Go By eample](https://gobyexample.com/) ```go= package main import "fmt" func main() { fmt.Println("hello golang") } ``` ## Debug ### Escape Analysis > varialbe 分佈,要麻是 stack, 不然就是 heap 而如果變數無法在 scope 被釋放,就只能配到 heap 去,所以可以進行逃逸分析來了解是否對 GC 造成巨大壓力 ```bash= go build -gcflags=-m . > output.txt 2>&1 ``` ### OOM > golang 透過 GC 回收 memory 可以設定 GOMEMLIMIT 來限制最大 memory 用量,避免 OOM - [GOMEMLIMIT](https://hsleep.medium.com/optimizing-memory-management-in-go-with-gomemlimit-da8ed23e2dcb) ## Document - [Printf](https://golang.org/pkg/fmt/) - [Deepin array and slice](https://www.sohamkamani.com/golang/arrays-vs-slices/) - [各種go地雷的分析用法](https://pjchender.dev/golang/pointers) ## Topic Document - [OAuth2 PKCE (Proof Key for Code Exchange) for line login](https://engineering.linecorp.com/zh-hant/blog/pkce-line-login/) - [pseudo-versions](https://jfrog.com/blog/go-big-with-pseudo-versions-and-gocenter/) - [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) - [Go Modules with private git repo](https://medium.com/cloud-native-the-gathering/go-modules-with-private-git-repositories-dfe795068db4) - [為什麼 Pointer Receiver 不能使用 Value Type 賦值給 Interface Value](https://mileslin.github.io/2020/08/Golang/%E7%82%BA%E4%BB%80%E9%BA%BC-Pointer-Receiver-%E4%B8%8D%E8%83%BD%E4%BD%BF%E7%94%A8-Value-Type-%E8%B3%A6%E5%80%BC%E7%B5%A6-Interface-Value/) - [HowTo High performance http client](https://www.loginradius.com/blog/async/tune-the-go-http-client-for-high-performance/) - [Understanding golang nil](https://www.jianshu.com/p/dd80f6be7969) - [Channel use cases and pattern](https://go101.org/article/channel-use-cases.html) - [Golang struct receiver should use Value or Pointer](https://blog.wu-boy.com/2017/05/go-struct-method-pointer-or-value/) - [Golang JWT Encode/Decode](https://medium.com/%E4%BC%81%E9%B5%9D%E4%B9%9F%E6%87%82%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88/golang-json-web-tokens-jwt-olang-json-web-tokens-jwt-%E7%A4%BA%E7%AF%84-225b377e0f79) - [Golang by example](https://golangbyexample.com/) - [Accept interface, return structs](https://bryanftan.medium.com/accept-interfaces-return-structs-in-go-d4cab29a301b) - [Go module upgrade](https://golang.cafe/blog/how-to-upgrade-golang-dependencies.html) - [Go Repository pattern](https://medium.easyread.co/golang-clean-archithecture-efd6d7c43047) - [Go Interpreter Pattern](https://medium.com/@MTrax/golang-interpreter-pattern-411e6d5e4a30) - [Go Design Pattern](https://medium.com/towardsdev/desing-patters-in-golang-24a142d2cc91) - [Go Design Pattern 2](https://medium.com/selectfrom/golang-design-patterns-in-kubernetes-6e74d6caba4)