# :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)

### Check Go module version
> go version -m gopls (不知道該 binary 是那個 go version build 出來時,可以這樣確認)

## 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` 來顯示裝那些相依套件

- `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)