前言 最近一兩個月開始寫比較多 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 
buffered chhannel 
 
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  這邊整理過來的一些資料