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