Golang學習筆記

tags: 後端
目錄

Golang特點

為什麼 Golang 適合做為網頁後端程式的語言呢?

由於 Golang 有以下的優點:

  1. 易學易用
    • 基本上是強化版的 C 語言,都以核心語法短小精要著稱
  2. 是靜態型別語言
    • 很多程式的錯誤在編譯期就會挑出來,相對易於除錯
  3. 編譯速度很快
    • 帶動整個開發的流程更快速
  4. 支援垃圾回收
    • 網頁程式較接近應用程式,而非系統程式,垃圾回收在這個情境下不算缺點;此外,使用垃圾回收可簡化程式碼
  5. 內建共時性的語法
    • goroutine 比起傳統的執行緒 (thread) 來說輕量得多,在高負載時所需開銷更少
  6. 是跨平台的
    • 只要程式中不碰到 C 函式庫,在 Windows (或 Mac) 寫好的 Golang 網頁程式,可以不經修改就直接發布在 GNU/Linux 伺服器上
  7. 專案不需額外的設定檔
    • 在專案中,只要放 Golang 程式碼和一些 assets 即可運作,所需的工具皆內建在 Golang 主程式中,省去學習專案設罝的功夫
  8. 沒有死硬的程式架構
    • 用 Golang 寫網頁程式思維上接近微框架 (micro-framework),只要少數樣板程式碼就可以寫出網頁程式,也不限定可用的第三方函式庫

但 Golang 並非完美無缺,以下是要考量的點:

  • Golang 並非完整的物件導向 (object-oriented) 語言,頂多是基於物件的 (object-based) 語言
  • Golang 的語言特性相對少:這是 Golang 時常被攻擊的點,這只能靠自己調整寫程式的習慣
  • 在一些情境下,Golang 程式碼相對笨拙冗餘,像是排序 (sorting)

開始一個專案

  1. 安裝好 go 以及設定 $GOPATH 環境
  2. VSCode設置
  3. 目錄結構
--src 放置專案的原始碼檔案
--pkg 放置編譯後生成的包 / 庫檔案
--bin 放置編譯後生成的可執行檔案
  1. mod
go mod init 初始化
go mod tidy 整理模組
  1. 測試囉(Gin、Mysql)
go get github.com/gin-gonic/gin
go get github.com/go-sql-driver/mysql

main.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

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

Go的資料型態

Go的資料類別一共分為四大類:

  1. 基本型別(Basic type): 數字、字串、布林值
  2. 聚合型別(Aggregate type): 陣列、結構
  3. 參照型別(Reference type): 指標、slice、map、function、channel
  4. 介面型別(Interface type)

變數宣告

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 // int
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.

字串

var mystr01 string = "\\r\\n"
等於
var mystr02 string = `\r\n`

輸出:\r\n

`` 表示一個多行的字串

陣列

陣列

// 第一種方法
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", //最後這裡要有逗號
}

切片

為一個左閉右開的結構

//宣告一個空的切片
var arr []int    //默認值為nil

運用make( []Type, size, cap )指定類型、長度、容量,
建立一個容量為10,目前長度為3的切片:

make([]int, 3, 10)    //make( []Type, size, cap )
  • 輸出
arr[0:2] 
//-->[1 2] 結尾索引不算在內
  • append
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
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]

字典

  • 宣告
// 第一種方法
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
  • 新增 / 讀取 / 更新 / 刪除
scores["math"] = 95
scores["math"] = 100 //若已存在,直接更新
delete( scores, "math" )
fmt.Println(scores["math"]) //不存在則返回value-type的0值
//-->100
  • 判斷是否存在字典裡
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
  • 巢狀字典
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型別的物件
參考資料

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.
type Message struct {
	Sender  string `json:"sender"`
	RoomId  string `json:"roomId"`
	Content string `json:"content"`
	Time    string `json:"time"`
}
  • 放入資料產生[]byte 格式的 json 資料
jsonMessage, _ := json.Marshal(&Message{Sender: c.id, RoomId: c.roomId, Content: string(message), Time: time})
  • 解回struct物件
var msg Message
json.Unmarshal(message, &msg)

指標

跟C語言一樣,Go語言也有指標。

func zero( x *int ) {
    *x = 0
}

func main() {
    x := 5
    zero( &x )
    fmt.Println( x )
}

介面 interface

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

func main() {
    sum := 0
    for i := 0; i < 10; i++ {
        sum += i
    }
    fmt.Println(sum)
}

跟 C 或者 Java 中一樣,可以讓前置、後置語句為空。

func main() {
    sum := 1
    for ; sum < 1000; {
        sum += sum
    }
    fmt.Println(sum)
}

基於此可以省略分號:C 的 while 在 Go 中叫做 「for」。

func main() {
    sum := 1
    for sum < 1000 {
        sum += sum
    }
    fmt.Println(sum)
}

如果省略了迴圈條件,迴圈就不會結束,因此可以用更簡潔地形式表達無窮迴圈。

func main() {
    for {
        fmt.Println("Hello World")
    }
}

陣列尋訪

可以這樣尋訪

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獲取陣列元素數量

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

更精簡一點

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變數我們使用佔位符(_)替代。

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提前退出循環。

func main() {
    for i := 0; i < 10; i++ {
        if i > 5 {
            break       
        }
        fmt.Println(i)
    }
}

如果有多重迴圈,可以指定要跳出哪一個迴圈,但需要指定標籤。

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忽略之後的程式碼,直接執行下一次迭代。

func main() {
    for i := 1; i <= 10; i++ {
        if i < 6 {
            continue
        }
        fmt.Println(i)
    }
}

同樣的如果有多重迴圈,也可以指定標籤。

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 」,但是不建議使用,會讓程式的結構變得很糟糕。

func main() {
    i := 0
HERE:
    fmt.Print(i)
    i++
    if i < 10 {
        goto HERE
    }
}

defer、panic、recover

此範例文章取自openhome.cc

就許多現代語言而言,例外處理機制是基本特性之一,然而,例外處理是好是壞,一直以來存在著各種不同的意見,在 Go 語言中,沒有例外處理機制,取而代之的,是運用 defer、panic、recover 來滿足類似的處理需求。

defer

在 Go 語言中,可以使用 defer 指定某個函式延遲執行,那麼延遲到哪個時機?簡單來說,在函式 return語句之後準備返回呼叫的函式之前,例如:

  • 延遲效果
func myfunc() {
    fmt.Println("B")
}

func main() {
    defer myfunc()
    fmt.Println("A")
}

輸出

A
B
  • 可在返回之前修改返回值
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
  • 變數的快照
func main() {
    name := "go"
    defer fmt.Println(name) // 變數name的值被記住了,所以會输出go

    name = "python"
    fmt.Println(name)      // 输出: python
}

輸出

python
go
  • 應用
  1. 反序調用
    如果有多個函式被 defer,那麼在函式 return 前,會依 defer 的相反順序執行,也就是 LIFO,例如:
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
  1. defer 與 return
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()

func f() {
    r := getResource()  //0,獲取資源

    defer r.release()  //1,釋放資源
    ......
    if ... {
        ...
        return
    }
    ......
    if ... {
        ...
        return
    }
    ......
    return
}

以下是清除資源的範例:

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 的效應也會一路往回傳播。

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 來進行類似的錯誤處理。例如,將上頭的範例,再修改為:

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

if 條件一 {    
  分支一
} else if 條件二 {
  分支二
} else if 條件 ... {
  分支 ...
} else {
  分支 else
}

// { 必須與if..在同一行

&& : 且
|| : 或

在 if 裡允許先運行一個表達式,取得變數後再來做判斷:

func main() {
    if age := 20;age > 18 {
        fmt.Println("已成年")
    }
}

switch

與一般的switch宣告方法一樣,條件不能重複

  • 一個case多個條件
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 後可接函數
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. 一般用法
func add( x int, y int ) int {
	return x + y
}

func main() {
	fmt.Println( add( 42, 13 ) )
}

當兩個或多個連續的函數命名參數是同一類型,則除了最後一個類型之外,其他都可以省略。
所以如果參數的型態都一樣的話,可以精簡為:

func add( x, y int ) int {  
    return x + y  
}  
 
func main() {  
    fmt.Println( add( 42, 13 ) )  
}  
  1. 多數值返回

函數可以返回任意數量的返回值,這個函數返回了兩個字串。

func swap(x, y string) (string, string) {  
    return y, x  
}  
 
func main() {  
    a, b := swap("hello", "world")  
    fmt.Println(a, b)  
}
// 輸出結果 world hello
  1. 命名返回值

在 Go 中,函數可以返回多個「結果參數」,而不僅僅是一個值。它們可以像變數那樣命名和使用。
如果命名了返回值參數,一個沒有參數的 return 語句,會將當前的值作為返回值返回。以這個程式碼為例,sum int 表示宣告整數 sum ,將參數 17 放入 sum 中,x, y int 宣告整數 x,y 在下面使用,由於 return 沒有設定返回值,這邊程式就將 x,y 都回傳了,所以結果會出現 7 10。

func split(sum int) (x, y int) {  
    x = sum * 4 / 9  
    y = sum - x  
    return  
}  
 
func main() {  
    fmt.Println(split(17))  
}

Goroutine

要使用Goroutine只要在呼叫的函數前面加一個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 先跑完。

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

先來看個沒有啟用 Goroutine,卻要寫個龜兔賽跑遊戲的例子,你可能是這麼寫的:

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 啟動執行:

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 來改寫以上的範例:

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 之間通信

ch_name := make(chan <TYPE>{,NUM})    //類型與大小

資料流向

  • 向Channel傳入:Ch <- DATA
  • 從Channel讀取:DATA := <- Ch
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

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語言為了讓團隊開發能夠更加的簡單,他統一了程式碼的風格,如果沒有遵照他的規範寫的話,你再如何編譯都不會成功。
    以下為錯誤的程式碼風格

    ​​​​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 }
    

    以下為正確的寫法

    ​​​​package main
    ​​​​import "fmt"
    ​​​​func main() {
    ​​​​    i:= 1
    ​​​​    fmt.Println("Hello World", i)
    ​​​​}
    

    為了保持程式碼的乾淨,你宣告了一個變數,但是卻沒有使用,Go 語言連編譯都不會讓你編譯。舉例來說,變數 i 並沒有被使用。

    ​​​​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. 非強制性編譯風格建議
    以下程式碼可以正常的編譯,但是很醜不好閱讀。

    ​​​​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

GO gRPC

官方 - Quick start
範例 - Hello ,gRPC
How we use gRPC to build a client/server system in Go
比起 JSON 更方便、更快速、更簡短的 Protobuf 格式
API 文件就是你的伺服器,REST 的另一個選擇:gRPC

gRPC and HTTP APIs

比較 gRPC 服務與 HTTP API
同時提供HTTP接口
gRPC-Web:envoy
如果兩邊都想要 - gRPC Gateway

參考資料

[1] Go (Golang) 適合初學者做為網頁後端程式嗎?
[2] Golang — GOROOT、GOPATH、Go-Modules-三者的關係介紹
[3] GeeksforGeeks: Data Types in Go
[4] 初學Golang30天
[5] Go 语言设计与实现 - make 和 new
[6] Opencc Go
[7] 使用 Golang 打造 Web 應用程式
[8] 五分钟理解golang的init函数
[9] Go标准库:Go template用法详解
[10] How to use local go modules with golang with examples
[11] Go并发编程模型:主动停止goroutine
[12] Go gin框架入门教程
[13] Golang 套件初始化流程
[14] Go語言變數的生命週期
[15] 使用golang的mysql无法插入emoji表情的问题
[16] Go语言高级编程(Advanced Go Programming)
[17] Golang中range的使用方法及注意事项
[18] Go語言101

範例補充資料

gorilla/websocket - example:chat
Build a Realtime Chat Server With Go and WebSockets
Go Websocket 長連線