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