# Golang學習筆記 ###### tags: `RD1` :::spoiler 目錄 [TOC] ::: ## Golang特點 為什麼 Golang 適合做為網頁後端程式的語言呢? 由於 Golang 有以下的優點: - Golang 易學易用:Golang 基本上是強化版的 C 語言,都以核心語法短小精要著稱 - Golang 是靜態型別語言:很多程式的錯誤在編譯期就會挑出來,相對易於除錯 - Golang 編譯速度很快:帶動整個開發的流程更快速 - Golang 支援垃圾回收:網頁程式較接近應用程式,而非系統程式,垃圾回收在這個情境下不算缺點;此外,使用垃圾回收可簡化程式碼 - Golang 內建共時性的語法:goroutine 比起傳統的執行緒 (thread) 來說輕量得多,在高負載時所需開銷更少 - Golang 是跨平台的:只要程式中不碰到 C 函式庫,在 Windows (或 Mac) 寫好的 Golang 網頁程式,可以不經修改就直接發布在 GNU/Linux 伺服器上 - Golang 的專案不需額外的設定檔:在專案中,只要放 Golang 程式碼和一些 assets 即可運作,所需的工具皆內建在 Golang 主程式中,省去學習專案設罝的功夫 - Golang 沒有死硬的程式架構:用 Golang 寫網頁程式思維上接近微框架 (micro-framework),只要少數樣板程式碼就可以寫出網頁程式,也不限定可用的第三方函式庫 但 Golang 並非完美無缺,以下是要考量的點: - Golang 並非完整的物件導向 (object-oriented) 語言,頂多是基於物件的 (object-based) 語言 - Golang 的語言特性相對少:這是 Golang 時常被攻擊的點,這只能靠自己調整寫程式的習慣 - 在一些情境下,Golang 程式碼相對笨拙冗餘,像是排序 (sorting) ## 開始一個專案 1. 安裝好 go 以及設定 $GOPATH 環境 2. [VSCode設置](https://maiyang.me/post/2018-09-14-tips-vscode/) 3. 目錄結構 ``` --src 放置專案的原始碼檔案 --pkg 放置編譯後生成的包 / 庫檔案 --bin 放置編譯後生成的可執行檔案 ``` 3. mod ``` go mod init 初始化 go mod tidy 整理模組 ``` 4. 測試囉(Gin、Mysql) ``` go get github.com/gin-gonic/gin go get github.com/go-sql-driver/mysql ``` main.go ```go package main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run(":8000") } ``` mysql.go ```go package main import ( "database/sql" _ "github.com/go-sql-driver/mysql" //只引用該套件的init函數 ) func main() { db, err := sql.Open("mysql", "root:root@tcp(mysql)/test?charset=utf8") defer db.Close() //插入資料,使用預處理避免發生injection stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?") checkErr(err) _, err = stmt.Exec("astaxie", "研發部門", "2012-12-09") checkErr(err) } func checkErr(err error) { if err != nil { panic(err) } } ``` > 兩者都為 package main 代表他們本質上是一隻程式 只是分為不同檔案 > 不同的package之間需分為不同資料夾,並互相引用: "module_name/floder_name" ex.import router(別名) "main/routes" [Go 實作 Restful API](https://medium.com/skyler-record/go-實作-restful-api-2a32210adeaf) ## Go的資料型態 Go的資料類別一共分為四大類: 1. 基本型別(Basic type): 數字、字串、布林值 2. 聚合型別(Aggregate type): 陣列、結構 3. 參照型別(Reference type): 指標、slice、map、function、channel 4. 介面型別(Interface type) ### 變數宣告 ```go var a // 不定型別的變數 var a int // 宣告成 int var msg string // 宣告成 string var a int = 10 // 初始化同時宣告 var a = 10 // 會自動幫你判定為整數型別 var a, b int // a 跟 b 都是 intvar a, b = 0 var a int , b string var a, b, c int = 1, 2, 3 var a, b, c = 1, 2, 3 var( a bool = false // 記得要不同行,不然會錯 b int c = "hello" ) // 在函數中,「:=」 簡潔賦值語句在明確類型的地方,可以替代 var 定義。 //「:=」 結構不能使用在函數外,函數外的每個語法都必須以關鍵字開始。 // := 只能用在宣告 var msg = "Hello World" 等於 msg := "Hello World" //自動判定型態 a := 0 a, b, c := 0, true, "tacolin" // 這樣就可以不同型別寫在同一行 _, b := 34, 35 // _(下劃線)是個特殊的變數名,任何賦予它的值都會被丟棄。 ``` ### 布林值 在Go中 bool 與 int 不能直接轉換,`true`,`false` 不直接等於 1 與 0 ### 整數 |型態 | 描述 | |----|----| |int8|8-bit signed integer| |int16|16-bit signed integer| |int32|32-bit signed integer| |int64|64-bit signed integer| |uint8|8-bit unsigned integer| |uint16|16-bit unsigned integer| |uint32|32-bit unsigned integer| |uint64|64-bit unsigned integer| |int |Both in and uint contain same size, either 32 or 64 bit.| |uint |Both in and uint contain same size, either 32 or 64 bit.| |rune |等價 unit32 ,表示一個Unicode字符| |byte|等價 uint8 ,表示一個ASCII字符| |uintptr|It is an unsigned integer type. Its width is not defined, but its can hold all the bits of a pointer value.| ### 浮點數 型態 | 描述 | |----|----| |float32|32-bit IEEE 754 floating-point number| |float64|64-bit IEEE 754 floating-point number| ### 複數 型態 | 描述 | |----|----| |complex64|Complex numbers which contain float32 as a real and imaginary component.| |complex128|Complex numbers which contain float64 as a real and imaginary component.| ### 字串 ```go var mystr01 string = "\\r\\n" 等於 var mystr02 string = `\r\n` 輸出:\r\n ``` ` `` ` 表示一個多行的字串 ### 陣列 #### 陣列 ```go // 第一種方法 var arr = [3]int{1,2,3} //%T = [3]int // 第二種方法 arr := [3]int{1,2,3} // 第三種方法 arr := [...]int{1,2,3} // 可以省略長度而採用`...`的方式,Go 會自動根據元素個數來計算長度 //注意類型為字串時 var arr = [3]string{ "first", "second", "third", //最後這裡要有逗號 } ``` #### 切片 為一個左閉右開的結構 ```go //宣告一個空的切片 var arr []int //默認值為nil ``` 運用`make( []Type, size, cap )`指定類型、長度、容量, 建立一個容量為10,目前長度為3的切片: ```go make([]int, 3, 10) //make( []Type, size, cap ) ``` * 輸出 ```go arr[0:2] //-->[1 2] 結尾索引不算在內 ``` * append ```go myarr := []int{1} // 追加一個元素 myarr = append(myarr, 2) // 追加多個元素 myarr = append(myarr, 3, 4) // 追加一個切片, ... 表示解包,不能省略 myarr = append(myarr, []int{7, 8}...) // 在開頭插入元素0 myarr = append([]int{0}, myarr[0:]...) //[0:]為開頭的話可省略 // 在中間插入一個切片(兩個元素) myarr = append(myarr[:5], append([]int{5, 6}, myarr[5:]...)...) fmt.Println(myarr) //--> [0 1 2 3 4 7 8] ``` * copy ```go slice1 := []int{1,2,3} slice2 := make([]int, 2) copy(slice2, slice1) fmt.Println(slice1, slice2) // 由於slice2容量只有2所以只有slice1[0:2]被複製過去 // 輸出結果: [1 2 3] [1 2] ``` ### 字典 * 宣告 ```go // 第一種方法 var scores map[string]int = map[string]int{"english": 80, "chinese": 85} // 第二種方法 scores := map[string]int{"english": 80, "chinese": 85} // 第三種方法 scores := make(map[string]int) scores["english"] = 80 scores["chinese"] = 85 ``` * 新增 / 讀取 / 更新 / 刪除 ```go scores["math"] = 95 scores["math"] = 100 //若已存在,直接更新 delete( scores, "math" ) fmt.Println(scores["math"]) //不存在則返回value-type的0值 //-->100 ``` * 判斷是否存在字典裡 ```go elements := map[string]string{ "H": "Hydrogen", "He": "Helium", "Li": "Lithium", "Be": "Beryllium" } value, isExist := elements["H"]; // value = Hydrogen, isExist = true value, isExist := elements["A"]; // value = "", isExist = false ``` * 巢狀字典 ```go elements := map[string]map[string]string{ "H": map[string]string{ "name":"Hydrogen", "state":"gas", }, "He": map[string]string{ "name":"Helium", "state":"gas", }, "Li": map[string]string{ "name":"Lithium", "state":"solid", }, "Be": map[string]string{ "name":"Beryllium", "state":"solid", }, "B": map[string]string{ "name":"Boron", "state":"solid", }, "C": map[string]string{ "name":"Carbon", "state":"solid", }, "N": map[string]string{ "name":"Nitrogen", "state":"gas", }, "O": map[string]string{ "name":"Oxygen", "state":"gas", }, "F": map[string]string{ "name":"Fluorine", "state":"gas", }, "Ne": map[string]string{ "name":"Neon", "state":"gas", }, } if el, ok := elements["Li"]; ok { fmt.Println(el["name"], el["state"]) } ``` ### Struct 自定義型別,struct裡可以放struct型別的物件 [參考資料](https://ithelp.ithome.com.tw/articles/10188100) ```go type person struct { name string height int } ``` #### json & struct * 宣告 Struct fields must start with upper case letter (exported) for the JSON package to see their value. ```go type Message struct { Sender string `json:"sender"` RoomId string `json:"roomId"` Content string `json:"content"` Time string `json:"time"` } ``` * 放入資料產生`[]byte` 格式的 json 資料 ```go jsonMessage, _ := json.Marshal(&Message{Sender: c.id, RoomId: c.roomId, Content: string(message), Time: time}) ``` * 解回struct物件 ```go var msg Message json.Unmarshal(message, &msg) ``` ### 指標 跟C語言一樣,Go語言也有指標。 ```go func zero( x *int ) { *x = 0 } func main() { x := 5 zero( &x ) fmt.Println( x ) } ``` ### 介面 interface ```go package main import "fmt" import "math" type geometry interface { area() float64 perimeter() float64 } type square struct { width, height float64 } type circle struct { radius float64 } func (s square) area() float64 { return s.width * s.height } func (s square) perimeter() float64 { return 2*s.width + 2*s.height } func (c circle) area() float64 { return math.Pi * c.radius * c.radius } func (c circle) perimeter() float64 { return 2 * math.Pi * c.radius } func measure(g geometry) { fmt.Println(g) fmt.Println(g.area()) fmt.Println(g.perimeter()) } func main() { s := square{width: 3, height: 4} c := circle{radius: 5} measure(s) measure(c) } ``` ## 控制語句 ### 迴圈 #### for Go只有一種迴圈關鍵字,就是for ```go func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) } ``` 跟 C 或者 Java 中一樣,可以讓前置、後置語句為空。 ```go func main() { sum := 1 for ; sum < 1000; { sum += sum } fmt.Println(sum) } ``` 基於此可以省略分號:C 的 while 在 Go 中叫做 「for」。 ```go func main() { sum := 1 for sum < 1000 { sum += sum } fmt.Println(sum) } ``` 如果省略了迴圈條件,迴圈就不會結束,因此可以用更簡潔地形式表達無窮迴圈。 ```go func main() { for { fmt.Println("Hello World") } } ``` #### 陣列尋訪 可以這樣尋訪 ```go var x [4]float64{ 23, 45, 33, 21 } var total float64 = 0 for i := 0; i < 4; i++ { total += x[i] } fmt.Println( total / float64(4)) ``` 使用`len`獲取陣列元素數量 ```go var x [4]float64{ 23, 45, 33, 21 } var total float64 = 0 for i := 0; i < len(x); i++ { total += x[i] } fmt.Println( total / float64(len(x))) ``` 更精簡一點 ```go var x [4]float64{ 23, 45, 33, 21 } var total float64 = 0 for i, value := range x { total += value } fmt.Println( total / float64(len(x))) ``` for迴圈前面的第一個變數意義為陣列索引(index),而後面變數代表該索引值所代表的陣列值。以上寫法會出錯,由於Go不允許沒有使用的變數出現在程式碼中,迴圈的i變數我們使用佔位符(_)替代。 ```go func main() { var x [4]float64{ 23, 45, 33, 21 } var total float64 = 0 for _, value := range x { total += value } fmt.Println( total / float64(len(x))) } ``` ### 分支 break、continute、goto #### break 可以利用break提前退出循環。 ```go func main() { for i := 0; i < 10; i++ { if i > 5 { break } fmt.Println(i) } } ``` 如果有多重迴圈,可以指定要跳出哪一個迴圈,但需要指定標籤。 ```go func main() { outer: // 標籤在此 for j := 0; j < 5; j++ { for i := 0; i < 10; i++ { if i > 6 { break outer } fmt.Println(i) } } } ``` #### continute continue忽略之後的程式碼,直接執行下一次迭代。 ```go func main() { for i := 1; i <= 10; i++ { if i < 6 { continue } fmt.Println(i) } } ```` 同樣的如果有多重迴圈,也可以指定標籤。 ```go func main() { outer: // 標籤在此 for i := 1; i < 10; i++ { for j := 1; j < 10; j++ { if i == j { continue outer } fmt.Println( "i: ", i, " j: ", j ); } } } ``` #### goto Go 語言跟 C 語言一樣也有「 goto 」,但是不建議使用,會讓程式的結構變得很糟糕。 ```go func main() { i := 0 HERE: fmt.Print(i) i++ if i < 10 { goto HERE } } ``` ### defer、panic、recover > 此範例文章取自[openhome.cc](https://openhome.cc/Gossip/Go/DeferPanicRecover.html) 就許多現代語言而言,例外處理機制是基本特性之一,然而,例外處理是好是壞,一直以來存在著各種不同的意見,在 Go 語言中,沒有例外處理機制,取而代之的,是運用 defer、panic、recover 來滿足類似的處理需求。 #### defer 在 Go 語言中,可以使用 defer 指定某個函式延遲執行,那麼延遲到哪個時機?簡單來說,在函式 return語句之後準備返回呼叫的函式之前,例如: * 延遲效果 ```go func myfunc() { fmt.Println("B") } func main() { defer myfunc() fmt.Println("A") } ``` 輸出 ```go A B ``` * 可在返回之前修改返回值 ```go package main import "fmt" func Triple(n int) (r int) { defer func() { r += n // 修改返回值 }() return n + n // <=> r = n + n; return } func main() { fmt.Println(Triple(5)) } ``` 輸出 ``` 15 ``` * 變數的快照 ```go func main() { name := "go" defer fmt.Println(name) // 變數name的值被記住了,所以會输出go name = "python" fmt.Println(name) // 输出: python } ``` 輸出 ``` python go ``` * 應用 1. 反序調用 如果有多個函式被 defer,那麼在函式 return 前,會依 defer 的相反順序執行,也就是 LIFO,例如: ```go package main import "fmt" func deferredFunc1() { fmt.Println("deferredFunc1") } func deferredFunc2() { fmt.Println("deferredFunc2") } func main() { defer deferredFunc1() defer deferredFunc2() fmt.Println("Hello, 世界") } // 輸出結果: Hello, 世界 deferredFunc2 deferredFunc1 ``` 2. defer 與 return ```go func f() { r := getResource() //0,獲取資源 ...... if ... { r.release() //1,釋放資源 return } ...... if ... { r.release() //2,釋放資源 return } ...... r.release() //3,釋放資源 return } ``` 使用 defer 後,不論在哪 return 都會執行 defer 後方的函數,如此便不用在每個return前寫上`r.release()` ```go func f() { r := getResource() //0,獲取資源 defer r.release() //1,釋放資源 ...... if ... { ... return } ...... if ... { ... return } ...... return } ``` 以下是清除資源的範例: ```go package main import ( "fmt" "os" ) func main() { f, err := os.Open("/tmp/dat") if err != nil { fmt.Println(err) return; } defer func() { // 延遲執行,而且函式 return 後一定會執行 if f != nil { f.Close() } }() b1 := make([]byte, 5) n1, err := f.Read(b1) if err != nil { fmt.Printf("%d bytes: %s\n", n1, string(b1)) // 處理讀取的內容.... } } ``` ### panic 恐慌中斷 如果在函式中執行 panic,那麼函式的流程就會中斷,若 A 函式呼叫了 B 函式,而 B 函式中呼叫了 panic,那麼 B 函式會從呼叫了 panic 的地方中斷,而 A 函式也會從呼叫了 B 函式的地方中斷,若有更深層的呼叫鏈,panic 的效應也會一路往回傳播。 ```go package main import ( "fmt" "os" ) func check(err error) { if err != nil { panic(err) } } func main() { f, err := os.Open("/tmp/dat") check(err) defer func() { if f != nil { f.Close() } }() b1 := make([]byte, 5) n1, err := f.Read(b1) check(err) fmt.Printf("%d bytes: %s\n", n1, string(b1)) } ``` 如果在開啟檔案時,就發生了錯誤,假設這是在一個很深的呼叫層次中發生,若你直接想撰寫程式,將 os.Open 的 error 逐層傳回,那會是一件很麻煩的事,此時直接發出 panic,就可以達到想要的目的。 ### recover 如果發生了 panic,而你必須做一些處理,可以使用 recover,這個函式必須在被 defer 的函式中執行才有效果,若在被 defer 的函式外執行,recover 一定是傳回 nil。 如果有設置 defer 函式,在發生了 panic 的情況下,被 defer 的函式一定會被執行,若當中執行了 recover,那麼 panic 就會被捕捉並作為 recover 的傳回值,那麼 panic 就不會一路往回傳播,除非你又呼叫了 panic。 因此,雖然 Go 語言中沒有例外處理機制,也可使用 defer、panic 與 recover 來進行類似的錯誤處理。例如,將上頭的範例,再修改為: ```go package main import ( "fmt" "os" ) func check(err error) { if err != nil { panic(err) } } func main() { f, err := os.Open("/tmp/dat") check(err) defer func() { if err := recover(); err != nil { fmt.Println(err) // 這已經是頂層的 UI 介面了,想以自己的方式呈現錯誤 } if f != nil { if err := f.Close(); err != nil { panic(err) // 示範再拋出 panic } } }() b1 := make([]byte, 5) n1, err := f.Read(b1) check(err) fmt.Printf("%d bytes: %s\n", n1, string(b1)) } ``` ### 條件判斷 #### if、else、else if ```go if 條件一 { 分支一 } else if 條件二 { 分支二 } else if 條件 ... { 分支 ... } else { 分支 else } // { 必須與if..在同一行 ``` `&&` : 且 `||` : 或 在 if 裡允許先運行一個表達式,取得變數後再來做判斷: ```go func main() { if age := 20;age > 18 { fmt.Println("已成年") } } ``` #### switch 與一般的switch宣告方法一樣,條件不能重複 * 一個case多個條件 ```go import "fmt" func main() { month := 2 switch month { case 3, 4, 5: fmt.Println("春天") case 6, 7, 8: fmt.Println("夏天") case 9, 10, 11: fmt.Println("秋天") case 12, 1, 2: fmt.Println("冬天") default: fmt.Println("輸入有誤...") } } ``` * switch 後可接函數 ```go import "fmt" // 判断一个同学是否有挂科记录的函数 // 返回值是布尔类型 func getResult(args ...int) bool { for _, i := range args { if i < 60 { return false } } return true } func main() { chinese := 80 english := 50 math := 100 switch getResult(chinese, english, math) { // case 后也必须 是布尔类型 case true: fmt.Println("该同学所有成绩都合格") case false: fmt.Println("该同学有挂科记录") } } ``` * switch 後面不接東西時就相當於if-else * 使用`fallthrough` 可以往下穿透一層,執行下一個case語句且**不用判斷條件**,但其必須為該case的最後一個語句,否則會錯誤 ## Go 函式 1. 一般用法 ```go func add( x int, y int ) int { return x + y } func main() { fmt.Println( add( 42, 13 ) ) } ``` 當兩個或多個連續的函數命名參數是同一類型,則除了最後一個類型之外,其他都可以省略。 所以如果參數的型態都一樣的話,可以精簡為: ```go func add( x, y int ) int { return x + y } func main() { fmt.Println( add( 42, 13 ) ) } ``` 2. 多數值返回 函數可以返回任意數量的返回值,這個函數返回了兩個字串。 ```go func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("hello", "world") fmt.Println(a, b) } // 輸出結果 world hello ``` 3. 命名返回值 在 Go 中,函數可以返回多個「結果參數」,而不僅僅是一個值。它們可以像變數那樣命名和使用。 如果命名了返回值參數,一個沒有參數的 return 語句,會將當前的值作為返回值返回。以這個程式碼為例,sum int 表示宣告整數 sum ,將參數 17 放入 sum 中,x, y int 宣告整數 x,y 在下面使用,由於 return 沒有設定返回值,這邊程式就將 x,y 都回傳了,所以結果會出現 7 10。 ```go func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return } func main() { fmt.Println(split(17)) } ``` ## Goroutine 要使用Goroutine只要在呼叫的函數前面加一個go關鍵字即可 ```go package main import "fmt" func f(n int) { for i := 0; i < 10; i++ { fmt.Println(n, ":", i) } } func main() { go f(0) } ``` 執行後會發現什麼東西都沒有印出,因為 goroutine 是平行處理的, 所以在還沒開始印 n 之前 main 這個主要的函式已經結束了。 使用內建的 time 函式讓 main 函式等 goroutine 先跑完。 ```go package main import ( "fmt" "time" ) func f(n int) { for i := 0; i < 10; i++ { fmt.Println(n, ":", i) } } func main() { go f(0) time.Sleep(time.Second * 1) // 暫停一秒鐘 } ``` ### 龜兔賽跑的範例 > 此龜兔賽跑範例文章引用自[openhome.cc/Go/Goroutine](https://openhome.cc/Gossip/Go/Goroutine.html) 先來看個沒有啟用 Goroutine,卻要寫個龜兔賽跑遊戲的例子,你可能是這麼寫的: ```go package main import ( "fmt" "math/rand" "time" ) func random(min, max int) int { rand.Seed(time.Now().Unix()) return rand.Intn(max-min) + min } func main() { flags := [...]bool{true, false} totalStep := 10 tortoiseStep := 0 hareStep := 0 fmt.Println("龜兔賽跑開始...") for tortoiseStep < totalStep && hareStep < totalStep { tortoiseStep++ fmt.Printf("烏龜跑了 %d 步...\n", tortoiseStep) isHareSleep := flags[random(1, 10)%2] if isHareSleep { fmt.Println("兔子睡著了zzzz") } else { hareStep += 2 fmt.Printf("兔子跑了 %d 步...\n", hareStep) } } } ``` 由於程式只有一個流程,所以只能將烏龜與兔子的行為混雜在這個流程中撰寫,而且為什麼每次都先遞增烏龜再遞增兔子步數呢?這樣對兔子很不公平啊!如果可以撰寫程式再啟動兩個流程,一個是烏龜流程,一個兔子流程,程式邏輯會比較清楚。 你可以將烏龜的流程與兔子的流程分別寫在一個函式中,並用 go 啟動執行: ```go package main import ( "fmt" "math/rand" "time" ) func random( min, max int ) int { rand.Seed( time.Now().Unix() ) return rand.Intn( max - min ) + min } func tortoise( totalStep int ) { for step := 1; step <= totalStep; step++ { fmt.Printf( "烏龜跑了 %d 步...\n", step ) } } func hare(totalStep int) { flags := [...]bool{true, false} step := 0 for step < totalStep { isHareSleep := flags[random(1, 10)%2] if isHareSleep { fmt.Println("兔子睡著了zzzz") } else { step += 2 fmt.Printf("兔子跑了 %d 步...\n", step) } } } func main() { totalStep := 10 go tortoise(totalStep) go hare(totalStep) time.Sleep(5 * time.Second) // 給予時間等待 Goroutine 完成 } ``` #### 使用sync.WaitGroup等待烏龜與兔子跑完 有沒有辦法知道 Goroutine 執行結束呢?實際上沒有任何方法可以得知,除非你主動設計一種機制,可以在 Goroutine 結束時執行通知,使用 Channel 是一種方式,這在之後的文件再說明,這邊先說明另一種方式,也就是使用 sync.WaitGroup。 sync.WaitGroup 可以用來等待一組 Goroutine 的完成,主流程中建立 sync.WaitGroup,並透過 Add 告知要等待的 Goroutine 數量,並使用 Wait 等待 Goroutine 結束,而每個 Goroutine 結束前,必須執行 sync.WaitGroup 的 Done 方法。 因此,我們可以使用 sync.WaitGroup 來改寫以上的範例: ```go package main import ( "fmt" "math/rand" "time" "sync" ) func random( min, max int ) int { rand.Seed( time.Now().Unix() ) return rand.Intn( max - min ) + min } func tortoise( totalStep int, wg *sync.WaitGroup ) { defer wg.Done() for step := 1; step <= totalStep; step++ { fmt.Printf( "烏龜跑了 %d 步...\n", step ) } } func hare(totalStep int, wg *sync.WaitGroup ) { defer wg.Done() flags := [...]bool{true, false} step := 0 for step < totalStep { isHareSleep := flags[random(1, 10)%2] if isHareSleep { fmt.Println("兔子睡著了zzzz") } else { step += 2 fmt.Printf("兔子跑了 %d 步...\n", step) } } } func main() { wg := new( sync.WaitGroup ) wg.Add( 2 ) totalStep := 10 go tortoise( totalStep, wg ) go hare( totalStep, wg ) time.Sleep(5 * time.Second) // 給予時間等待 Goroutine 完成 } ``` ## Channel 通過 Channel 可以讓 goroutine 之間通信 ```go ch_name := make(chan <TYPE>{,NUM}) //類型與大小 ``` ### 資料流向 * 向Channel傳入:`Ch <- DATA` * 從Channel讀取:`DATA := <- Ch` ```go func main() { messages := make(chan string) go func() { messages <- "ping" }() msg := <- messages fmt.Println( msg ) } ``` * 建立一個 channel(message) 用以傳輸字串 * 用 go 來 call goroutine 執行函式,傳 "ping" 到 messages 這個 channel 裡面 * 接著以 msg 負責接收 messages 的傳輸資料後印出 透過這個方法就可以簡單的讓 Goroutine 可以溝通 ### select 有一個類似 Switch 的流程控制「Select」,它只能應用於 Channel ```go package main import "time" import "fmt" func main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(time.Second * 1) c1 <- "one" }() go func() { time.Sleep(time.Second * 2) c2 <- "two" }() for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println("received", msg1) case msg2 := <-c2: fmt.Println("received", msg2) } } } ``` ## Go Coding Style 1. 強制編碼風格 Go語言為了讓團隊開發能夠更加的簡單,他統一了程式碼的風格,如果沒有遵照他的規範寫的話,你再如何編譯都不會成功。 以下為錯誤的程式碼風格 ```go package main import "fmt" func main() { i:= 1 fmt.Println("Hello World", i) } ``` 如果你左右括弧的寫法是像上面那樣,你將會看到下列的錯誤訊息 ``` syntax error: unexpected semicolon or newline before {non-declaration statement outside function body syntax error: unexpected } ``` 以下為正確的寫法 ```go package main import "fmt" func main() { i:= 1 fmt.Println("Hello World", i) } ``` 為了保持程式碼的乾淨,你宣告了一個變數,但是卻沒有使用,Go 語言連編譯都不會讓你編譯。舉例來說,變數 i 並沒有被使用。 ```go package main import "fmt" func main() { i := 1 fmt.Println("Hello World i") } ``` 你會出現下列錯誤訊息 ``` # command-line-arguments ./test.go:6:2: i declared but not used ``` 2. 非強制性編譯風格建議 以下程式碼可以正常的編譯,但是很醜不好閱讀。 ```go package main import "fmt" func main() { i:= 1 fmt.Println("Hello World", i)} ``` 我們可以利用`go fmt`指令幫忙整理程式碼編譯格式。 用法 `go fmt <filename>.go # 整理某個檔案` `go fmt *.go # 整理目錄下所有go檔案` `go fmt # 同上` 如果程式碼不需要調整他不會出現任何訊息,成功會出現你使用的程式檔名。 格式化工具幫你做到了下列事情: - 調整每一條語句的位置 - 重新擺放括弧的位置 - 以 tab 幫你縮排 - 添加空格 ## 套件 #### Go套件的一些規則 Go之所以會那麼簡潔,是因為它有一些預設的行為: * 大寫字母開頭的變數是可匯出的,也就是其它套件可以讀取的,是公有變數;小寫字母開頭的就是不可匯出的,是私有變數。 * 大寫字母開頭的函式也是一樣,相當於 class 中的帶 public 關鍵詞的公有函式;小寫字母開頭的就是有 private 關鍵詞的私有函式。 ## gRPC ![](https://www.grpc.io/img/landing-2.svg) ### GO gRPC [官方 - Quick start](https://www.grpc.io/docs/languages/go/quickstart/) [範例 - Hello ,gRPC](https://myapollo.com.tw/zh-tw/golang-grpc-tutorial-part-1/) [How we use gRPC to build a client/server system in Go](https://medium.com/pantomath/how-we-use-grpc-to-build-a-client-server-system-in-go-dd20045fa1c2) [比起 JSON 更方便、更快速、更簡短的 Protobuf 格式](https://yami.io/protobuf/) [API 文件就是你的伺服器,REST 的另一個選擇:gRPC](https://yami.io/grpc/) ### gRPC and HTTP APIs [比較 gRPC 服務與 HTTP API](https://docs.microsoft.com/zh-tw/aspnet/core/grpc/comparison?view=aspnetcore-3.1) [同時提供HTTP接口](https://segmentfault.com/a/1190000016601836) [gRPC-Web:envoy](https://ithelp.ithome.com.tw/articles/10244296) [如果兩邊都想要 - gRPC Gateway](https://ithelp.ithome.com.tw/articles/10243864) ## 參考資料 [1] [Go (Golang) 適合初學者做為網頁後端程式嗎?](https://michaelchen.tech/blog/golang-as-backend-language-for-beginners/) [2] [Golang — GOROOT、GOPATH、Go-Modules-三者的關係介紹](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-goroot-gopath-go-modules-%E4%B8%89%E8%80%85%E7%9A%84%E9%97%9C%E4%BF%82%E4%BB%8B%E7%B4%B9-d17481d7a655) [3] [GeeksforGeeks: Data Types in Go](https://www.geeksforgeeks.org/data-types-in-go/) [4] [初學Golang30天](https://ithelp.ithome.com.tw/users/20079210/ironman/721 ) [5] [Go 语言设计与实现 - make 和 new](https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-make-and-new/) [6] [Opencc Go](https://openhome.cc/Gossip/Go/) [7] [使用 Golang 打造 Web 應用程式](https://willh.gitbook.io/build-web-application-with-golang-zhtw/) [8] [五分钟理解golang的init函数](https://zhuanlan.zhihu.com/p/34211611) [9] [Go标准库:Go template用法详解](https://www.cnblogs.com/f-ck-need-u/p/10053124.html) [10] [How to use local go modules with golang with examples](https://brokencode.io/how-to-use-local-go-modules-with-golang-with-examples/) [11] [Go并发编程模型:主动停止goroutine](https://zhuanlan.zhihu.com/p/66659719) [12] [Go gin框架入门教程](https://www.tizi365.com/archives/244.html) [13] [Golang 套件初始化流程](https://www.jianshu.com/p/9ba805f07a95) [14] [Go語言變數的生命週期](https://www.itread01.com/content/1546733197.html) [15] [使用golang的mysql无法插入emoji表情的问题](https://blog.csdn.net/qq_43192269/article/details/103289623) [16] [Go语言高级编程(Advanced Go Programming)](https://chai2010.cn/advanced-go-programming-book/) [17] [Golang中range的使用方法及注意事项](https://studygolang.com/articles/12958) [18] [Go語言101](https://gfw.go101.org/article/101.html) ## 範例補充資料 [gorilla/websocket - example:chat](https://github.com/gorilla/websocket/tree/master/examples/chat) [Build a Realtime Chat Server With Go and WebSockets](https://scotch.io/bar-talk/build-a-realtime-chat-server-with-go-and-websockets) [Go Websocket 長連線](https://ithelp.ithome.com.tw/articles/10223666)