# Go 語言開發實戰從入門到進階:課程筆記 ###### tags: `後端` * 講師: Bo-Yi Wu * [課程大綱](https://docs.google.com/document/d/1uzs3Kav_KjItwY9uWcvoqHrdg7P6FScJRNu_AokGmsg/edit) * [課程PPT1 5/20](https://drive.google.com/file/d/1h7RYBI85MgDwBfm3RwoasDTJnCwEiXYZ/view) * [課程PPT2 5/27](https://drive.google.com/file/d/1R8LEtiWIHEyl9TDDgfll4guG_9ItYmSd/view) * [範例github1](https://github.com/go-training/workshop-20220520) * [範例github2](https://github.com/go-training/workshop-20220527) ``` 「這就很簡單,只要...然後...再來#($*&%」 *10秒內演示完畢* ``` ## 01. GO語言的優點 * 快速⼜簡單部署 * ⽀援跨平台編譯 * 保護原始程式碼 * 善⽤多核⼼處理 * 團隊學習曲線低 ## 02. 基本環境建置 ### VSCode * command+shift+p > Preferences:Open Setting(JSON) * command+shift+p > go install/Update > gopls(trace source code) * golangci-linter ## 03. Go command ### go build/go install * main.go置於./cmd/{fileName},go install安裝任何第三方執行檔名稱取決於{fileName},並且會放在~/go/bin中 ### go test * 匡選func後使用generate unit tests for function 功能可自動產生某func的簡單測試 * 測試檔命名建議使用原檔名後+"_test",測試func命名使用駝峰或底線TestHello or Test_hello * 測試某個func: `go test -v -run=TestFunctionName .` * `go test -v -bench .`後可抓取Benchmark開頭之test func ## 04. Package ### go mod * 如果今天同一個repo裡面有很多的小project,各自有自己的go.mod等等的,go work這東西就是可以直接在最外層執行每一包project裡面的main.go * "directory . outside modules listed in go.work or their selected dependencies"解法 => 到02-go-command => `go mod init 02-go-comand` => `go mod tidy` => `go work use .` * `go mod vendor` 用於產生套件的source code在 ./vendor資料夾下,可於無網路環境下使用,可以直接修改地方三套件的code來除錯,用完再移除就好 * `go mod tidy` 整理第三方.mod .sum 並下載於本地GOPATH/pkg ### package 特性 * func命名⼤⼩寫代表 public 或 private * 只能import要用的套件 * 不可引用相對路徑,使用.mod中的module的名稱 * `_ "packageUrl"` 只引用該套件的func init() ## 05. Go Build Constraints * 透過命名分系統:hello_darwin、hello_linux、hello_windows * 透過go build tag分系統或版本 ex. `//go:build darwin` `//go:build !go1.18` ## 06. Go 基礎 ### defer * 如果defer一個function,要注意參數的傳入。 ### map * 沒有順序性。for輪詢時,順序可能會跟想像中不一樣。 ### slice 範例07: example02/main.go/foo03、foo04 * 主要對記憶體位置做變動,容易變動到原始值(但array就採copy不會影響原值) * append後會產生一個新的記憶體位置 ### new/make 初始化值 * struct 的 `new(example)` 相當於 `&example{}` * `data := make([]int,0,s)` allocs效率最好 ### pointer 使用同樣的記憶體位置 ```go func (e *Example)SetExampleColor(color string){ // do some } ``` 不可使用copy方式改變值,無效 ```go func (e Example)SetExampleColor(color string){ // do some } ``` ## 07. struct ### 空struct的特殊用法 * 可能今天想要判斷某個東西是不是處理過了可以用`map[string]struct{}{}`,處理過的就放到這個map中,放空的strcut可以避免golang真的分配一個記憶體位置給他 * 如果單純想要放在channel裡面當作一個訊號驅動程式的進行,可以用`ch := make(chan struct{})` * 省記憶體空間,close一個struct{} channel相當於傳值進去(都是0) [範例](https://stackoverflow.com/questions/52035390/why-using-chan-struct-when-wait-something-done-not-chan-interface) * 最後一個也是比較不會這樣用的,`type empty strcut{}`,然後只想要去實作裡面的method ### type func(s *Struct) 可以做為option使用,例如: smtp.go ```go type Smtp struct { Address string `json:"address"` Port int64 `json:"port"` } type Option func(s *Smtp) func WithAddress(address string) Option { return func(s *Smtp) { s.Address = address } } func New2(opts ...Option) *Smtp { s := &Smtp{ Address: "127.0.0.1", Port: 1234, } for _, opt := range opts { opt(s) } return s } ``` main.go ```go func main() { s := smtp.New2() fmt.Println(s) // &{127.0.0.1 1234} s := smtp.New2(smtp.WithAddress("111.222.33")) fmt.Println(s) // &{111.222.33 1234} } ``` ## 08. interface{} 詳見10-interface->example02 在golang裡面interface會定義一些方法,但這些方法只有function名,也就是只有皮。struct會定義方法的實際邏輯。然後interface可以去接strcut,也就是把interface中定義好的方法透過struct做出來。 1. 定義(多個不同型態的)接口 2. extend 3. 實做不同接口 4. 判斷接口型態 ## 09. emuns ```go type StatusType int const ( StatusPrepare StatusType = iota StatusInitial StatusRunning StatusSuccess StatusFailure ) func main() { fmt.Println( StatusInitial, StatusRunning, StatusSuccess, StatusFailure, ) // 0, 1, 2, 3 } ``` ## 10. goroutine ### 原則 1. 開發library,不要在裡面使用goroutine 2. 確保goroutine會執行完畢,防止堆積 3. Check race condition at compile time ### sync.WaitGroup 等待多個goroutine程序執行完畢 ### sync.Lock 多個goroutine在同時存取相同值時,避免取到錯的或同時讀寫 需要將該變數上鎖 ### channel * Do not communicate by sharing memory; instead, share memory by communicating. * channel的三種狀態和三種操作結果 | 操作/狀態 | 空值(nil) | 已關閉 | 未關閉 | | -------- | -------- | -------- | -------- | | 關閉(close)| panic | panic | Success | | 寫資料| Blocking | panic | Blocking or Success | | 讀資料| Blocking | non blocking | Blocking or Success | #### unbuffer 同步用 有寫就要有讀(反之亦然),否則會deadlock 可用於等待某程序執行結束 ```go func main() { c := make(chan bool) go func() { fmt.Println("GO GO GO") c <- true }() <-c // 印出文字後才會繼續往下執行 } ``` #### buffer 異步用 寫完不讀也能繼續下去 ```go func main() { c := make(chan bool,1) go func() { fmt.Println("GO GO GO") c <- true }() <-c // 印出文字之前,func就執行完畢了 } ``` 但只讀不寫一樣會造成無窮等待 ```go func main() { c := make(chan bool,1) go func() { fmt.Println("GO GO GO") }() <-c // Compile Error: deadLock } ``` #### 限制讀寫方向 `func(ch <-chan int)` 只讀 `func(ch chan<- int)` 只寫 #### 讀寫範例 * 邊寫邊讀,讀完結束 ```go package main import ( "fmt" "sync" ) func main() { wg := sync.WaitGroup{} wg.Add(2) ch := make(chan int) // receive channel go func(ch <-chan int) { // for { // if v, ok := <-ch; ok { // fmt.Println(v) // } else { // // channel closed // break // } // } // wg.Done() // 跟上面相比,這個寫法更漂亮 for v := range ch { fmt.Println(v) } wg.Done() }(ch) // send channel go func(ch chan<- int) { ch <- 100 ch <- 101 close(ch) // 確保寫完後 正確關閉channel wg.Done() }(ch) wg.Wait() } ``` * 一次性寫一堆時確保全數執行 ```go func main() { outChan := make(chan string) errChan := make(chan error) finshChan := make(chan struct{}) wg := sync.WaitGroup{} wg.Add(100) for i := 0; i < 100; i++ { // 避免外面的參數影響到裡面,要把直傳進去go func裡 go func(num int, out chan<- string, err chan<- error) { defer wg.Done() if num/10 == 0 { // 如果可被10整除,是為錯誤拋出 err <- fmt.Errorf("err: %d", num) return } else { out <- strconv.Itoa(num) } }(i, outChan, errChan) } // 或是go Loop的那區也可以,但wait要移下去 go func() { wg.Wait() close(finshChan) }() Loop: for { select { case out := <-outChan: fmt.Println(out) case err := <-errChan: fmt.Println(err) case <-finshChan: close(outChan) close(errChan) fmt.Println("Done.") break Loop } } } // --printOut-- // 無順序的將0~99印出,中間被10整除的數印出err: ``` * 用於處理timeout ```go func job1(done chan bool) { time.Sleep(100 * time.Millisecond) // 模擬程序執行100毫秒 done <- true } func job2(done chan bool) { time.Sleep(600 * time.Millisecond) // 模擬程序執行600毫秒,超時 // 因為doJob已經結束(return)了,done可能被回收掉導致下一行寫不進去 // 如果done是unbuffer channel的話,會卡在背景裡結束不了 done <- true } func doJob(wg *sync.WaitGroup, f func(chan bool)) error { // done := make(chan bool) done := make(chan bool, 1) // 這裡設定為buffer=1 可以避免leak defer wg.Done() go f(done) select { case <-done: fmt.Println("done") return nil case <-time.After(500 * time.Millisecond): return fmt.Errorf("timeout") } } func main() { wg := &sync.WaitGroup{} wg.Add(2) go func() { if err := doJob(wg, job1); err != nil { fmt.Println(err) } }() go func() { if err := doJob(wg, job2); err != nil { fmt.Println(err) } }() wg.Wait() // --printOut-- // done // timeout } ``` ### goroutine leak #### goleak([package](https://github.com/uber-go/goleak)) 配合`go test`偵測程式碼內goroutine內的leak ```go // TestMain測試時會第一個被執行 func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ``` #### pprof 可參考[Oops!Golang - 讓我們來抓出吃資源的兇手!](https://ithelp.ithome.com.tw/articles/10235172) 1. 引用pprof套件 ```go _ "net/http/pprof" ``` 2. 起http服務(如果原本沒有的話) ```go go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() ``` 3. 直接查看 打開 http://localhost:6060 4. 產生檔案 ```shell go tool pprof http://localhost:6060/debug/pprof/heap ``` 5. 以圖表瀏覽 ```shell go tool pprof -http=:8080 /Users/$mypc/pprof/{剛剛產生的檔案名稱} ``` ## 11. graceful shotdown ### context 用context可以控制多個goroutine同時結束 ```go func main() { ctx, cancel := context.WithCancel(context.Background()) data := make(chan int, 100) go func(data chan<- int) { for i := 0; i < 100; i++ { data <- i } }(data) go worker(ctx, "node01", data) go worker(ctx, "node02", data) go worker(ctx, "node03", data) time.Sleep(5 * time.Second) fmt.Println("stop the gorutine") cancel() time.Sleep(5 * time.Second) } func worker(ctx context.Context, name string, data <-chan int) { for { select { case c := <-data: // 因為cancel後還是可能進到這邊,所以要對ctx.Err()進行處理,避免程序繼續執行 if ctx.Err() != nil { fmt.Println(name, "stop to handle data:", ctx.Err().Error()) return } fmt.Println(name, "got data value", c) time.Sleep(500 * time.Millisecond) case <-ctx.Done(): fmt.Println(name, "got the stop channel") return } } } ``` ### channel with context #### getHTTPResponse timeout control > homework: 04-context-timeout * 一般使用http request時,務必加上內建結構的timeout參數,避免一直連著線 ```go client := &http.Client{ Timeout: time.Second, } ``` * 也可以改由channel自行控制timeout,那麼上一點就不用加 ```go // 目前作業大失敗 ``` ### http service ## 12. consumer 架構 ### 實例 整理了很久,真的要進來看看 [Go 語言開發實戰從入門到進階: Consumer 實作](http://35.194.182.75:3000/IvTyLCm6RWuxU6MdokB2_A?view) ## 13. env config ### envconfig main.go (放到config/裡更好) ```go type Server struct { Debug bool Port int User string Users []string Rate float32 Timeout time.Duration ColorCodes map[string]int } func main() { var envfile string flag.StringVar(&envfile, "env-file", ".env", "Read in a file of environment variables") flag.Parse() godotenv.Load(envfile) cfg := &Server{} err := envconfig.Process("app", cfg) if err != nil { log.Fatal("invalid configuration") } fmt.Printf("%#v", cfg) } ``` .env ``` APP_USERS = bar,foo ``` ## 14. gin ### gin with http testing ``` "github.com/gavv/httpexpect" ``` ## 15. 專案架構建置 ### Makefile ## 16.加碼 ## 參考連結 [How To Write Unit Tests in Go](https://www.digitalocean.com/community/tutorials/how-to-write-unit-tests-in-go-using-go-test-and-the-testing-package)