前言 最近一兩個月開始寫比較多 Go 的專案,所以就把在寫 Go 時覺得應該要先知道的資訊記錄下來,這篇目前不會紀錄跟測試相關的,測試會再額外拉出來介紹。
strcut 和 receiver 的內容在之前的學習 Golang 的心得 - Receiver 就已經有提到過,這邊會快速帶過。整篇內容不會講太多細節,主要是可以清楚了解 Go 有哪些比較特別的用法,有些主題的原理我會再額外開文章去轉寫詳細內容。
struct 在 Go 裡面並沒有 class 的概念,取而代之的是 struct,有學過 C / C++ 對這東西應該很了解,基本上就是一種資料結構,而在 Go 裡面會大量用到 struct。
直接來看一個 struct 使用範例,就會看到印出 {jack}
出現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" type User struct { Name string } func main () { user := User{ Name: "jack" , } fmt.Println(user) }
如果想更詳細看到 struct 對應的欄位名稱,可以改用 fmt.Println("%#v\n", user)
,就可以看到 main.User{Name:"jack"}
這個結果出現。
receiver 接著若我想用 function 去修改我的名字的話,可以這麼做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" type User struct { Name string } func (user *User) changeName () { user.Name = "hi" } func main () { user := User{ Name: "jack" , } user.changeName() fmt.Printf("%#v\n" , user) }
可以看到特別的地方在於 function name 前面有一個類似參數的東西,那個叫做 receiver,另一個是 pointer 的部分,詳細的內容建議到學習 Golang 的心得 - Receiver 了解一下,裡面也有提到 Go 裡面是只有存在 pass by value,但以 map & slice 來說他們 copy 的是 pointer value,而不是資料本身,換句話說 map & slice 傳到 function 裡面做修改時是會影響外面的。
interface interface 可以來定義執行的動作,Go 是 duck typing 的一種類型,只要當前的方法和屬性有符合 interface 定義的結構,那就可以被使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport "fmt" type User struct { Name string } func (user *User) changeName (newName string ) { user.Name = newName } type Action interface { changeName(newName string ) } func doSomething (a Action, newName string ) { a.changeName(newName) } func main () { user := User{ Name: "jack" , } doSomething(&user, "hi2" ) fmt.Printf("%#v\n" , user) }
但這邊會看到一個比較特別的是用 &user
傳進去才可以使用,那是因為 changeName
這個 function 是 pointer type 的 User 實作的,並不是 value type 的 User 實作的,也就是 *User 有 changeName,但 User 沒有 changeName 可以使用,更詳細的之後再開一篇來說明。
callback 在 Go 中 function 是 First-class function,所以 function 可以被當作參數儲存下來。
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { a := func () { fmt.Println("cool" ) } a() }
也就意味著可以當成 callback 的方式去運行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func cool (inp string , cb func (result string ) ) { newStr := fmt.Sprintf("%s:%s" , inp, "hihihi" ) cb(newStr) } func main () { cool("yo?" , func (result string ) { fmt.Println(result) }) }
defer Go 中有一個 defer 方法,可以讓你 defer 後面接著 function 在執行的 function 的 scope 結束前去執行,直接來看範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func cool () { fmt.Println("yoyo" ) } func main () { fmt.Println("start" ) defer cool() fmt.Println("end" ) }
通常都會用在讀檔完成後,去用 defer 呼叫 f.close
,確保會把檔案給關閉。另外 defer 是 LIFO 的概念,也就是以 stack 的概念去看待。再來一個比較特別的用法,因為 defer 接收的參數是 function,所以可以透過在 defer 的 function 裡面回傳 function 用來計算執行 defer 本身 function 執行時間,類似以下方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "time" ) func cool () func () { fmt.Println(time.Now()) return func () { fmt.Println(time.Now()) } } func main () { defer cool()() time.Sleep(2 * time.Second) }
panic & recovery 在 Go 裡面可以用 panic 的方式直接終止程式運行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" ) func cool () { panic ("bad" ) } func main () { cool() fmt.Println("yoo?" ) }
即便改用 goroutine 的方式,整個程式還是會被終止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" "time" ) func cool () { panic ("bad" ) } func main () { go cool() time.Sleep(2 * time.Second) fmt.Println("yoo?" ) }
那麼被終止就會有對應可以回復的方式,就是透過 recovery 去接錯誤,但 recovery 只能用在 defer 接的 function 後面,而且一定要在 panic 之前呼叫才可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" ) func main () { defer func () { err := recover () fmt.Println("got error" ) fmt.Println(err) }() panic ("bad" ) fmt.Println("yoo?" ) }
但要注意的是,panic 後面得程式是不會繼續執行下去的,另外 panic & recovery 是有 scope 關係的,如果上面的程式用別的 goroutine 去執行 panic 則不會正確抓到,如下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" ) func cool () { panic ("bad" ) } func main () { defer func () { err := recover () fmt.Println("== got error ==" ) fmt.Println(err) fmt.Println("== got error ==" ) }() go cool() }
若是放到同個 scope 則可以運作正常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "fmt" "time" ) func cool () { defer func () { err := recover () fmt.Println("== got error ==" ) fmt.Println(err) fmt.Println("== got error ==" ) }() panic ("bad" ) } func main () { go cool() time.Sleep(1 * time.Second) }
init 通常在寫 class 的語言時,會習慣有 construct 的東西存在,當在 new 一個東西時去執行一些動作。只是 Go 沒有 class,且用 package 的概念,但還是相對類似的東西可以使用,也就是 init
,會發現以下程式不用實際去呼叫 init
這個 function 也能被執行到。
1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "fmt" ) func init () { fmt.Println("this is init" ) } func main () {}
以執行的順序來說,即使在上面有初始化一些資料,init 也會蓋過
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" ) var a = "1" func init () { a = "123" fmt.Println("this is init" ) } func main () { fmt.Println(a) }
buffered / unbuffered channel channel 主要是被設計在不同 goroutine 之間溝通的一種方式,並不是採用以往認知的共享記憶體,然後還要設計去限制一次只能有一個 thread 去對共享記憶體中的資料做讀寫這種複雜的方式,在 Go 裡面不同 goroutine 的溝通是更加簡單的。
先來對名詞簡單定義一下,後面會有更完整的總結說明。
unbuffered channel: 無法指定 channel 大小
buffered channel: 可以指定 channel 大小
再來對語法簡單說明一下
<- ch
代表是從 channel 中讀出資料
ch <-
代表是把資料塞到 channel 中
unbuffered channel 先來簡單看一個範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string ) go func () { time.Sleep(2 * time.Second) fmt.Println(<-ch) }() fmt.Println("start" ) ch <- "11" fmt.Println("end" ) }
這個範例除了 main thread goroutine 之外,還用了 go 開了一個 goroutine 出來,那印出的順序是按照順序的,也就說明這是一個同步行為,接著我們試著拿掉中間 go 的部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" ) func main () { ch := make (chan string ) fmt.Println("start" ) ch <- "11" fmt.Println("end" ) }
可以發現在執行 ch <- "11"
那一行就噴出 fatal error 了,原因是 unbufferd channel 是同步的關係,所以是會 block 當前 goroutine 的,以這個 case 來說,我們只有 main goroutine,並沒有其他 goroutine,就代表沒有其他地方可以執行讀取 channel 的指令,整個程式就會壞掉。
這也是在網路上常看到說,unbuffered chhanel 的讀寫必須是要一組的,有個地方讀,就要有個地方寫,不過我們再看一個範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" ) func main () { ch := make (chan string ) fmt.Println("start" ) go func () { for { } }() ch <- "11" fmt.Println("end" ) }
會發現這個範例只有寫入,卻沒有讀取,但不會噴出 error,雖然還沒讀到原始碼,但 Go 應該是認定雖然 main goroutine blocked,但有其他 goroutine 還在運行,代表期待其他 goroutine 會讀取這個 channel 資料,既然還有 goroutine 還活著,整個程式就不會陷入 deadlock。
所以實際上判定不是說,一定要有讀寫一組,而是當你用了一邊的讀/寫,那麼 Go 就期待有另一個地方也執行對應的寫/讀,若完全沒有 goroutine 存在,就代表不會有另一邊行為的出現,就會陷入 deadlock。
buffered channel 再來說到 buffered channel,直接來看範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" ) func main () { ch := make (chan string , 2 ) fmt.Println("start" ) ch <- "11" ch <- "11" fmt.Println("end" ) }
可以看到這個 case 跟前一個不同,是不需要額外開 goroutine 出來的,那如果我們塞往 channel 多塞一筆資料呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" ) func main () { ch := make (chan string , 2 ) fmt.Println("start" ) ch <- "11" ch <- "11" ch <- "11" fmt.Println("end" ) }
會發現情況變得跟 unbuffered 的情況一樣,這時候因為沒有其他 goroutine,所以就出現 deadlock,所以一樣故意加一個新的 goroutine,他就不會噴出 error,如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" ) func main () { ch := make (chan string , 2 ) fmt.Println("start" ) go func () { for { } }() ch <- "11" ch <- "11" ch <- "11" fmt.Println("end" ) }
所以 buffered channel 的特性,在塞滿之前是不會期待有其他 goroutine 去對 channel 操作,也意味這 buffered channel 在滿之前,會是非同步的行為,滿了之後就會 block 當前 goroutine,行為等同於 unbuffered channel,那如果在沒滿和滿之間的話呢?其實是可以在當前 goroutine 直接去做操作,如下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" ) func main () { ch := make (chan string , 2 ) fmt.Println("start" ) ch <- "11" fmt.Println(<-ch) ch <- "12" fmt.Println(<-ch) ch <- "13" fmt.Println(<-ch) fmt.Println("end" ) }
但另一個特別的點是,如果 buffered channel 裡面是沒有任何資料的話,使用 <- ch
也是會 block 當前的 goroutine,unbuffered channel 也是一樣的邏輯,如下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string , 2 ) go func () { fmt.Println("got it" ) time.Sleep(2 * time.Second) ch <- "hi" }() <-ch fmt.Println("end" ) }
可以看到開一個新的 goroutine 要去寫資料進去,但原本的 main goroutine 就停在 <-ch
,直到把資料寫進去 channel,main goroutine 才繼續執行下去,接著若我把中間 goroutine 拿掉的話,則會出現 deadlock,因為已經沒有任何 goroutine 存在,也就代表不可能有人可以把資料寫到 channel。
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "fmt" ) func main () { ch := make (chan string , 2 ) <-ch fmt.Println("end" ) }
簡單總結
unbuffered channel 當執行讀寫其中一個動作,會 block 當前 goroutine,若同時沒有其他 goroutine 則會陷入 deadlock
buffered chhannel 當 channel 沒滿的時候,是可以在同一個 goroutine 中讀寫多次 若 channel 是滿的時後,則會 block 當前 goroutine,若同時沒有其他 goroutine 則會陷入 deadlock。 若 channel 是空的時候,執行讀取也會 block 當前 goroutine,若同時沒有其他 goroutine 也會陷入 deadlock。
sync flow 直接先看以下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" ) func main () { go func () { fmt.Println("cool" ) }() fmt.Println("end" ) }
可以看到最終只有 end 被印出來,並沒有等待另一個 goroutine 中的 cool
,那是因為 main goroutine 已經結束,所以就跳出整個程式,這 part 要討論的是要如何去把同步流程,等到 cool
出來之後,整個程式才結束執行。
Channel 如果要讓程式停下來等,就可以利用 unbuffered channel block 的機制去實現,如下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" ) func main () { done := make (chan bool ) go func () { fmt.Println("cool" ) done <- true }() <-done fmt.Println("end" ) }
WaitGroup 另一個是 WaitGroup,主要提供 Add
Wait
Done
三個 function,只要 Add
多少次,就得需要做對應次數的 Done
,否則 Wait
的那一行就會一直等下去,簡單來說。
Add (int): 增加幾次計數
Done: 等同於 Add (-1) 的概念
Wait: blocked 直到 Add & Done 總合起來為零為止
先看下面正常使用的範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "sync" ) func main () { var wg sync.WaitGroup wg.Add(1 ) go func () { fmt.Println("cool" ) wg.Done() }() wg.Wait() fmt.Println("end" ) }
若如果有兩個 Add
,配合一個 Done
的會就會卡住,如下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "sync" ) func main () { var wg sync.WaitGroup wg.Add(2 ) go func () { fmt.Println("cool" ) wg.Done() }() wg.Wait() fmt.Println("end" ) }
從這範例可以發現跟 channel 的機制其實很相似,以這個 case 來說,已經沒有任何 goroutine 可以執行 Done
的動作,就會被歸類在 deadlock 了。
context 在寫 Go 時,可以很常看到 function 第一個參數就是 context,然後會一直被傳下去。
而這個 context 基本上是被設計同步不同 goroutine 流程或是夾帶資訊到不同 goroutine / function 之中的一個東西,我們先個看個 context 可以如何使用去夾帶資訊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "context" "fmt" ) func cool (ctx context.Context) { fmt.Println(ctx.Value("aa" )) } func main () { ctx := context.Background() ctx = context.WithValue(ctx, "aa" , "123" ) cool(ctx) }
上面範例就是把一個 key-value 綁在 context 傳下去,讓其他接收到的人都可以讀取同樣的資訊,其實像是在 Go http server,每一個請求都是開一個新的 goroutine,並把對應的 request body 資訊綁在 context 裡面往下傳,所以我們才可以直接在 contex 去讀取請求。
另一個是同步流程,假設我們想一次停止所有 goroutine,就很適合用 context 是去處理,如下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "context" "fmt" "time" ) func main () { ctx, cancel := context.WithCancel(context.Background()) go func (ctx context.Context) { fmt.Println("start-1" ) <-ctx.Done() fmt.Println(time.Now()) fmt.Println("end-1" ) }(ctx) go func (ctx context.Context) { fmt.Println("start-2" ) <-ctx.Done() fmt.Println(time.Now()) fmt.Println("end-2" ) }(ctx) time.Sleep(1 * time.Second) cancel() time.Sleep(1 * time.Second) }
select select 能夠監聽多個 channel 的讀寫狀況,若 channel 都沒有任何動作,就會 block 當前 goroutine,如下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string ) go func () { fmt.Println("start" ) select { case value := <-ch: fmt.Println(value) } fmt.Println("end" ) }() time.Sleep(2 * time.Second) }
若我在 Sleep 前把資料寫到 channel,那個 goroutine 就會監聽到這個動作,並往後執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string ) go func () { fmt.Println("start" ) select { case value := <-ch: fmt.Println(value) } fmt.Println("end" ) }() ch <- "cool" time.Sleep(2 * time.Second) }
但比較特別的點是 select 可以有一個 default case,當執行的當下沒有任何 channel 有動作,那就會執行 default 的部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string ) go func () { fmt.Println("start" ) select { case value := <-ch: fmt.Println(value) default : fmt.Println("default" ) } fmt.Println("end" ) }() time.Sleep(2 * time.Second) }
接著另一個有趣的點,當 select 搭配不同類型的 channel 會有不同的結果,關鍵取決於 channel 當下是否 block。以 unbuffered channel 來說,不管在 case 讀寫,都是屬於 block 行為,就不會觸發那條 case 發生,如下範例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string ) go func () { fmt.Println("start" ) select { case ch <- "cool" : fmt.Println("put" ) } fmt.Println("end" ) }() time.Sleep(2 * time.Second) }
若要讓他往下執行,就得需要對應的讀取動作才可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string ) go func () { fmt.Println("start" ) select { case ch <- "cool" : fmt.Println("put" ) } fmt.Println("end" ) }() <-ch time.Sleep(2 * time.Second) }
所以換到 buffered channel,在 channel 沒滿之前,case 是可以被正常觸發的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string , 2 ) go func () { fmt.Println("start" ) select { case ch <- "cool" : fmt.Println("put" ) } fmt.Println("end" ) }() time.Sleep(2 * time.Second) }
若 channel 是已滿的情況,再額外塞入時就會跟 unbuffered channel 一樣會被 blocked,一樣會需要其他 goroutine 先去讀取才可以觸發那條 case,我們先來看被 blocked 的情況
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string , 2 ) ch <- "cool" ch <- "cool" go func () { fmt.Println("start" ) select { case ch <- "cool" : fmt.Println("put" ) } fmt.Println("end" ) }() time.Sleep(2 * time.Second) }
一樣要解除這個情況,需要去把讀出 channel 資訊才可以繼續往下執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string , 2 ) ch <- "cool" ch <- "cool" go func () { fmt.Println("start" ) select { case ch <- "cool" : fmt.Println("put" ) } fmt.Println("end" ) }() <-ch time.Sleep(2 * time.Second) }
所以當下 case 都被 blocked 的話,且又有 default case 存在,就會一併跑到 default 去執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "time" ) func main () { ch := make (chan string , 2 ) ch <- "cool" ch <- "cool" go func () { fmt.Println("start" ) select { case ch <- "cool" : fmt.Println("put" ) default : fmt.Println("default" ) } fmt.Println("end" ) }() time.Sleep(2 * time.Second) }
所以在使用 select 要小心監聽的 channel 的情況,若有加上 default 的條件,對於 channel 會不會 block 就需要更加理解,否則可能全部都走到 default 去了,另外如果當 select 中存在兩者一樣的 case 則是會隨機挑一條去執行,這個網路上查基本上都會有,這邊就不附範例程式碼。
References 主要是從個人邊學邊紀錄在 Github Issuse 這邊整理過來的一些資料