[Go] The Little Go Book 概論

程式語言:Go
The Little Go Book 官網
Go 官網
Go 規格
線上 Go 編譯工具

簡介:Go 的基本語法應用

Terminal 基本指令 (Env Path 需有 C:\Go\bin)
go             # 顯示可用指令
go build       # 編譯執行檔於當前目錄
go clean       # 將 build 產生的檔案都刪除 (install 的不會刪)
go doc         # 顯示 help 文件,例: go doc fmt
                   # godoc -http=:6060 可到 http://localhost:6060 檢視文件。
go env         # 顯示環境變數
                   # GOROOT: Go 的安裝位置
                   # GOPATH: Go 的工作目錄
go bug         # 回報 bug
go fix         # 修正移植到新版本 GO 的錯誤 code
go fmt         # 排版代碼
go generate    # 用於批量執行命令
go get         # 安裝套件,可直接使用 github 網址
go install     # 產生執行檔於 $GOPATH/bin,或是安裝 package 於 $GOPATH/pkg
go list        # 列出所有套件。例 go list ./... (... 表示在目錄下所有檔案與目錄)
go run         # 直接執行 Go code,go run -x main.go 可提供詳細資訊
go test        # 自動讀取名為 *_test.go 的文件,並產生可運行測試的執行檔
go tool        # 執行特定工具,例:fix or vet
go version     # 版本
go vet         # 静態檢查,並回報可能的 bug
# 可使用瀏覽器連接 http://localhost:5000 或 http://主機IP:5000 查詢文件
godoc -http=:5000

# 可檢查 test 覆蓋率
go test -coverprofile=c.out
go tool cover -html=c.out

# 觀察性能的瓶頸,線上的時間表示以下所有區塊運行的時間
go test -bench . -cpuprofile cpu.out
go tool pprof cpu.out
web # 用瀏覽器觀看圖,需安裝 Graphviz

# race 檢測
go run -race main.go

# trace 程式碼
# 產生 trace file 三種方法
runtime/trace.Start
net/http/pprof package
go test -trace
# 其中之一的用法
go test -trace trace.out pkg
# 用瀏覽器觀看資料
go tool trace trace.out

官方介紹一整套用於診斷 Go 程序中的邏輯和性能問題的 API 和工具

基本概念
  • Go 是需要編譯、靜態型別、具有類似於 C 的語法與擁有 Garbage Collection 等特性的語言
  • Go 語言裡,程式的進入點是在 main package 裡面的 main function
  • import 關鍵字可以用來引用相關套件
    package main
    
    import (
        "fmt"
    )
    
    func main() {
        fmt.Println("123")
    }
    
    Import declaration          Local name of Sin
    
    import   "lib/math"         math.Sin
    import m "lib/math"         m.Sin
    import . "lib/math"         Sin
    import _ "lib/math"         only for init
    
    • 如果 import 了套件, 卻沒有使用,會出現錯誤
  • function 外的每個語法塊都必須以關鍵字開始( var 、 func 、等等)
  • Go 沒有物件或繼承,也沒有許多物件導向的概念,比如說多形或重載

變數與宣告
  • function 內如果宣告了變數,卻沒有使用,會出現錯誤
  • 宣告變數若沒提供初始值,會有 default 值
    • int: 0
    • bool: false
    • string: ""
  • 各種宣告方法如下
    package main
    
    var ag int = 3 // 可行,但很多餘,可拿掉 int,由值推論
    var bg int     // 宣告成 int
    var cg = 10    // 自行推論 type
    var dg, eg int // dg 跟 eg 都是 int
    // fg := 0 // 此方法無法宣告 global variable
    var ( // 記得要不同行,不然會錯
        gg bool = false // 可行,但很多餘,可拿掉 bool,由值推論
        hg int
        ig = "hello"
        jg = '字' // rune
        kg = `\n原始字串,可用在 regexp
    可多行,但會自動去掉換行的\r
    保證在所有平台上的值都是一樣的,例:Windows系統會把回車和換行一起放入文本文件中`
    )
    
    func main() {
        println(ag, bg, cg, dg, eg, gg, hg, ig, jg, kg)
    
        var al int = 3 // 可行,但很多餘,可拿掉 int,由值推論
        var bl int     // 宣告成 int
        var cl = 10    // 自行推論 type
        var dl, el int // dl 跟 el 都是 int
        fl := 0
        gl, hl, il := 0, true, "tacolin" // 這樣就可以不同型別寫在同一行
        var (                            // 記得要不同行,不然會錯
            jl bool = false // 可行,但很多餘,可拿掉 bool,由值推論
            kl int
            ll = "hello"
        )
    
        println(al, bl, cl, dl, el, fl, gl, hl, il, jl, kl, ll)
    }
    
  • 要記住 := 同時用於宣告變數以及為它賦值,而變數不能被宣告兩次
    但這種情況是允許的,因為至少有一個新變數,但 power 的 type 仍無法被改變
    而若是在不同的 scope 中,則會被視為重新宣告新變數
    package main
    
    func main() {
        power := 1000
        name, power := "Goku", 9000
        println(name, power)
    
        if true {
            power := 2000
            println(power)
        }
        println(power)
    }
    
  • _ 空白識別符號
    • 特別用在返回值不需要被實際指派的時候使用
      可以不管返回的類型,重複的使用 _ 識別符號,無需事先宣告
  • 基本類型
    • bool
    • string
    • int, int8, int16, int32, int64
    • uint uint8, uint16, uint32, uint64, uintptr
    • byte // uint8 的別名
    • rune // int32 的別名,也代表一個 Unicode 碼
    • float32, float64, complex64, complex128
  • const
    • 宣告如下
      package main
      
      //Pi 3.14
      const Pi = 3.14
      
      func main() {
          println("Happy", Pi, "Day")
      
          const World = "世界"
          println("Hello", World)
      
          const Truth bool = true
          println("Go rules?", Truth)
      }
      
    • 數值常數是高精度的值
      一個未指定類型的常數將由上下文來決定其類型
      package main
      
      func needInt(x int) int { return x/20 + 1 }
      func needFloat(x float64) float64 {
          return x * 0.1
      }
      
      func main() {
          const (
              Big   = 1 << 100
              Small = Big >> 99
          )
      
          println(needInt(Small))
          println(needFloat(Small))
          // overflow
          // println(needInt(Big))
          println(needFloat(Big))
      }
      
  • pointer
    • 宣告方法
      package main
      
      func main() {
          a := 1
          println(&a)
          change(&a)
          println(a)
      }
      
      func change(b *int) {
          *b++
      }
      

function 宣告
  • function 是一級型別 (first-class function)
  • 可以用在任何地方 - 當作一個欄位型別、參數、回傳值
  • 宣告方法
    package main
    
    // 輸入一個變數,無回傳變數
    func log(message string) {
    }
    
    // 輸入兩個變數,回傳一個變數
    func add(a int, b int) int {
        return a + b
    }
    
    // 輸入兩個變數,可合併簡寫,回傳一個變數
    func minus(a, b int) int {
        return a - b
    }
    
    // 輸入一個變數,回傳兩個變數
    func power(name string) (int, bool) {
        return 1, true
    }
    
    // 無輸入變數,回傳兩個變數,可事先宣告
    func positin() (x, y int) {
        x = 1
        y = 2
        return
    }
    
    func main() {
        // _ 空白識別符號是特別用在返回值不需要被實際指派的時候使用
        // 可以不管返回的類型,重複的使用 _ 識別符號
        // 但無需宣告其 type
        _ = add(1, 2)
        power, _ := power("goku")
        println(power)
    }
    
  • function 內不能再有 function,除非綁定在變數上,也就是 closure
    package main
    
    import "fmt"
    
    func adder() func(int) int {
        sum := 0
        f := func(x int) int {
            sum += x
            return sum
        }
        return f
    }
    
    func main() {
        pos, neg := adder(), adder()
        for i := 0; i < 10; i++ {
            fmt.Println(pos(i), neg(-2*i))
        }
    }
    
  • function 傳遞參數方式
    • 除了 Map, slices
    • 其餘皆是 copy 方式,甚至是 array,若希望更改值,請用 pointer
    • 但使用 slice 的 append 時,若 cap 不夠時,會被重新分配新的底層 array

control
  • for
    package main
         
    func main() {
        sum := 0
        for i := 0; i < 10; i++ {
            sum += i
        }
        println(sum)
    
        sum = 1
        // for ; sum < 1000; { 前後可為空,但 go fmt 後會被移除,類似 while
        for sum < 1000 {
            sum += sum
        }
        println(sum)
    
        sum = 1
        // 無窮迴圈
        for {
            sum += 1
            if sum == 100 {
                break
            }
        }
        println(sum)
    
        scores := [4]int{9001, 9333, 212, 33}
        // for in array
        for index, value := range scores {
            println(index, value)
        }
    
        // count
        n := 0
        for range "Hello, 世界" {
                n++
        }
        println(n)
    }
    
  • if
    package main
    
    func main() {
        i := 0
        if i == 0 {
            println(i)
        }
    
        // 初始化 i,此時的 i 作用域僅在 if else 範圍之內
        if i := 2; i == 2 {
            println(i) // 2
        }
        println(i) // 0
    
        if i > 0 {
        } else {
            println(i)
        }
    }
    
  • switch
    package main
    
    func main() {
        i := 2
        switch i {
        case 1:
            // 執行下一個
            fallthrough
        case 2:
            println("two")
        case 3, 4: // 3 or 4
            println("three, four")
        default:
            println("number")
        }
    
        switch {
        case i == 1 || i == 2:
            println("one or two")
        case i == 4:
            println("four")
        case i > 5 && i < 7:
            println("six")
        default:
            println("number")
        }
    }
    

struct
  • 宣告方法
    package main
    
    import (
        "fmt"
    )
    
    // Saiyan struct
    type Saiyan struct {
        Name  string
        Power int
    }
    
    // 事先宣告值
    var a = Saiyan{
        Name:  "a",
        Power: 9000,
    }
    
    func main() {
        // 後來指定值
        b := Saiyan{}
        b.Name = "b"
        b.Power = 1000
    
        // 依順序指定
        c := Saiyan{"c", 8000}
    
        // default 值
        d := Saiyan{}
    
        // 指標宣告
        e := new(Saiyan)
        // 結果同上
        f := &Saiyan{}
    
        fmt.Println(a, b, c, d, e, f)
    }
    
  • method
    package main
    
    import "fmt"
    
    type Saiyan struct {
        Name  string
        Power int
    }
    
    func (s *Saiyan) Super() {
        s.Power += 10000
    }
    
    func (s Saiyan) Lower() {
        s.Power -= 100
    }
    
    func main() {
        a := Saiyan{"a", 9001}
        a.Super()
        a.Lower() // 不會改變值
    
        // 結果同上
        b := &Saiyan{"b", 9001}
        b.Super()
        b.Lower() // 不會改變值
    
        fmt.Println(a.Power, b.Power) // 將列印出 19001
    }
    
    可以利用 type 對基本類型增加 method
    package main
    
    import (
        "fmt"
        "math"
    )
    
    type MyFloat float64
    
    func (f MyFloat) Abs() float64 {
        if f < 0 {
            return float64(-f)
        }
        return float64(f)
    }
    
    func main() {
        f := MyFloat(-math.Sqrt2)
        fmt.Println(f.Abs())
    }
    
  • 包含同類型
    package main
    
    import (
        "fmt"
    )
    
    // Saiyan struct
    type Saiyan struct {
        Name   string
        Power  int
        Father *Saiyan //include 自己必須用指標才可以
    }
    
    func main() {
        a := &Saiyan{
            Name:  "Gohan",
            Power: 1000,
            Father: &Saiyan{
                Name:   "Goku",
                Power:  9001,
                Father: nil,
            },
        }
        fmt.Println(a)
    }
    
  • Composition
    • 因為沒有提供明確的欄位名稱,所以可以透過組合隱性的存取這個欄位和 method
      但 Go 的編譯器仍會給他一個欄位名稱,但在上層若有同樣的屬性,例:Name,會以上層為主
      package main
      
      import (
          "fmt"
      )
      
      type Person struct {
          Name string
      }
      
      func (p *Person) Introduce() {
          fmt.Printf("Hi, I'm %s\n", p.Name)
      }
      
      type Saiyan struct {
          *Person
          //Name string // 會覆寫掉 Person 的 Name
          Power int
      }
      
      func main() {
          a := &Saiyan{
              Person: &Person{"a"},
              Power:  9001,
          }
          a.Introduce()
          fmt.Println(a.Name)
          fmt.Println(a.Person.Name)
      }
      
  • 可視性
    • 可被外部引用
      • 宣告類型或 method 時以大寫開頭
    • 不可被外部引用,同一個套件中的程式碼才能夠存取這些欄位
      • 宣告類型或 method 時以小寫開頭

Map、Array、Slice
  • Array
    • 宣告後即固定大小
    • 宣告方法
      package main
      
      func main() {
          var a [10]int
          a[0] = 339
          println(len(a)) // 10
      
          b := [4]int{9001, 9333, 212, 33}
          for index, value := range b {
              println(index, value)
          }
      }
      
  • Slice
    • 宣告後仍可變動大小
    • 類似 python 的 list
    • 宣告方法
      package main
      
      import "fmt"
      
      func main() {
          // 已有初始值
          a := []string{"leto", "jessica", "paul"}
          fmt.Println(a)
      
          // 宣告長度是 10 ,容量是 10 的 slice
          b := make([]bool, 10)
          fmt.Println(b)
      
          // 宣告長度是 0 ,容量是 0 的 slice,等同 nil,只能用 append 新增
          var c []string
          fmt.Println(c)
      
          // 宣告長度是 0 ,容量是 20 的 slice
          d := make([]int, 0, 20)
          fmt.Println(d)
      }
      
      
    • 賦值方法
      package main
      
      import "fmt"
      
      func main() {
          // 建立一個長度是 0 ,容量是 10 的 slice
          a := make([]int, 0, 10)
          // c[7] = 9033 無法這麼做,因長度為 0
          a = append(a, 5)
          fmt.Println(len(a), a) // 1 [5]
          a = a[0:8]             // 為了將值放在 c[7],需先擴展,前提是容量夠
          a[7] = 9033
          fmt.Println(a) // [5 0 0 0 0 0 0 9033]
      
          b := make([]int, 5)
          b = append(b, 9332) // 因長度已有 5,所以是 append 到第六個
          fmt.Println(b)      // [0 0 0 0 0 9332]
      }
      
    • 每次擴展的大小
      package main
      
      import "fmt"
      
      func main() {
          scores := make([]int, 0, 5)
          c := cap(scores)
          fmt.Println(c)
      
          for i := 0; i < 25; i++ {
              scores = append(scores, i)
      
              // 如果容量改變了
              // Go 為了容納新的資料,會增加陣列的長度
              // window64 是每次兩倍,所以是 5, 10, 20, 40
              // window32 不見得是每次兩倍,而是 5, 12, 24, 48
              if cap(scores) != c {
                  c = cap(scores)
                  fmt.Println(c)
              }
          }
      }
      
    • 值會被改變,可用 copy 預防
      package main
      
      import "fmt"
      
      func main() {
          scores := []int{1, 2, 3, 4, 5}
          slice := scores[2:4]
          slice[0] = 999
          fmt.Println(scores) // [1 2 999 4 5]
      }
      
    • Go 不支援負索引值
  • Map
    • 類似 python 的 dictionary
    • 宣告方法
      package main
      
      import (
          "fmt"
      )
      
      func main() {
          a := make(map[string]int)
          a["goku"] = 9001
          fmt.Println(len(a), a) // 1 map[goku:9001]
      
          //利用雙賦值,檢查是否存在
          power, exists := a["vegeta"]
          fmt.Println(power, exists) // 0 false
      
          // 刪除,沒有任何的回傳,可以呼叫一個不存在的 key
          delete(a, "goku")
          delete(a, "noexist")
          fmt.Println(len(a), a) // 0 map[]
      
          // 預先定義初始大小,有助於提升效能
          b := make(map[string]int, 100)
          fmt.Println(len(b), b) // 0 map[]
      
          c := map[string]int{
              "goku":  9001,
              "gohan": 2044,
          }
          // 無順序性
          for key, value := range c {
              fmt.Println(key, value)
          }
      
          // nil,此時無法賦值,需先用 make
          var e map[string]int
          fmt.Println(e == nil) // true
          e = make(map[string]int)
      }
      
    • 無法直接對 map 中的 struct 做出修改
      因 map 中的 value 是不可尋址的
      原本 struct 的資料可能在地址A,但當 map 擴展時地址A就不是原來的資料,所以go不允许寫入數據
      兩種解法:用 struct pointer or 重新賦值
      但在 Go2 可能會可行
      package main
      
      import (
          "fmt"
      )
      
      type s struct {
          a int
      }
      
      func main() {
          m1 := make(map[string]s)
          m1["k"] = s{a: 5}
          fmt.Printf("%v\n", m1)
      
          //重新賦值
          tmp := m1["k"]
          tmp.a = 8
          m1["k"] = tmp
          fmt.Printf("%v\n", m1)
      
          //cannot assign to struct field m["k"].a in map
          //m["k"].a = 8
      
          //struct pointer
          m2 := make(map[string]*s)
          m2["k"] = &s{a: 5}
          fmt.Printf("%v\n", m2["k"])
      
          m2["k"].a = 8
          fmt.Printf("%v\n", m2["k"])
      }
      
      

package
  • 位置:$GOPATH/src
  • package 與 folder 建議同名,可以不同名,但一個資料夾只能有一個 pakcage(不包含 sub-folder)
    import 使用的是路徑,而使用時需用宣告的 package 名字
    import xxx/xxxx/folderName
    
    packageName.function()
    
  • 檔案不見得需同名,而且可以多個
  • 範例
    • 架構
      $GOPATH/src
          - shopping
            pricecheck.go
            - db
              models.go
              func.go
          - main
            main.go
      
    • 程式碼
      • main.go
        package main
        
        import (
            "fmt"
            "shopping"
        )
        
        func main() {
            fmt.Println(shopping.PriceCheck(4343))
        }
        
      • pricecheck.go
        package shopping
        
        import (
            "shopping/db"
        )
        
        func PriceCheck(itemId int) (float64, bool) {
            item := db.LoadItem(itemId)
            if item == nil {
                return 0, false
            }
            return item.Price, true
        }
        
      • models.go
        package db
        
        type Item struct {
            Price float64
        }
        
      • func.go
        package db
        
        func LoadItem(id int) *Item {
            return &Item{
                Price: 9.001,
            }
        }
        
    • 避免循環引用,可使用 interface
      • 共享套件的重要規則就是,不應該從會引用自己的套件引用任何東西
        例:不該引用 shopping 套件或其他子套件中的任何東西
    • 可視性
      • 可被外部引用
        • 宣告類型或 function 時以大寫開頭
      • 不可被外部引用,同一個套件中的程式碼才能夠存取這些欄位
        • 宣告類型或 function 時以小寫開頭

interface
  • 用於多種變化的接口
  • 宣告方法
    package main
    
    import (
        "fmt"
    )
    
    func whatType(i interface{}) {
        // 運用 switch 決定做什麼事
        switch i.(type) {
        case int:
            fmt.Println("int")
        case bool:
            fmt.Println("bool")
        default:
            fmt.Println("ohters")
        }
    }
    
    func add(a interface{}, b interface{}) interface{} {
        // 若已知 type,可直接轉態
        return a.(int) + b.(int)
    }
    
    func main() {
        a, b := 1, 3
        whatType(a)
        fmt.Println(add(a, b))
    }
    
    package main
    
    import "fmt"
    
    type Logger interface {
        Log(message string)
    }
    
    type SqlLogger struct{}
    
    func (SqlLogger) Log(m string) {
        fmt.Println("SqlLogger", m)
    }
    
    type ConsoleLogger struct{}
    
    func (ConsoleLogger) Log(m string) {
        fmt.Println("ConsoleLogger", m)
    }
    
    type FileLogger struct{}
    
    func (FileLogger) Log(m string) {
        fmt.Println("FileLogger", m)
    }
    
    type Server struct {
        logger Logger
    }
    
    func process(logger Logger) {
        logger.Log("hello!")
    }
    
    func main() {
        a := Server{
            logger: SqlLogger{},
        }
        a.logger.Log("123")
        process(a.logger)
    
        b := Server{
            logger: ConsoleLogger{},
        }
        b.logger.Log("123")
        process(b.logger)
    
        c := Server{
            logger: FileLogger{},
        }
        c.logger.Log("123")
        process(c.logger)
    }
    
  • 在 Go 中,interface 的連結是隱性的
    • 如範例,如果結構有一個 function Log,且參數是 string,並且沒有回傳值,便會當作是一個 Logger
  • struct 的型態會影響 interface 的判斷
    The Go Playground
    package main
    
    type Interface interface {
        method()
    }
    
    type S1 struct{}
    func (s S1) method() {}
    
    type S2 struct{}
    func (s *S2) method() {}
    
    type S3 struct{}
    func (s S3) methodV() {}
    func (s *S3) methodP() {}
    
    func main() {
        s1 := S1{}
        _ = Interface(s1)
    
        s1p := &S1{}
        _ = Interface(s1p)
        
        s2 := S2{}
        _ = Interface(s2) // S2 does not implement Interface (method method has pointer receiver)
    
        s2p := &S2{}
        _ = Interface(s2p)
    
        // 但若只是單純的呼叫 method,Go 將會自動轉換
        // 因 Go 可以單純將變數依其型參轉換,進而當作參數傳入 method
        // 在這裡並不需要判斷是什麼類型,擁有什麼樣的 method
        s3 := S3{}
        s3.methodV()
        s3.methodP()
        
        s3p := &S3{}
        s3p.methodV()
        s3p.methodP()
        
        // 因直接呼叫 S3{} 是不可尋址,它只是個暫時性的位置
        // 所以無法呼叫對應的 pointer method
        S3{}.methodV()
        S3{}.methodP() // cannot call pointer method on S3 literal
        (&S3{}).methodV()
        (&S3{}).methodP()
    }
    
    原因在於
    指向 value 的 pointer 可以是很多種類型,所以無法反推
    但 pointer 指向的 value 卻是唯一的,所以可以確定
    The Go Playground
    package main
    
    import (
        "fmt"
    )
    
    type Interface interface {
        test()
    }
    
    type Interface2 interface {
        test()
        test2()
    }
    
    type S1 struct{}
    func (s *S1) test() {}
    func (s *S1) test2() {}
    
    func main() {
        s1p := &S1{}
        fmt.Printf("%T\n", s1p)
        //*main.S1
    
        s1i := Interface(&S1{})
        fmt.Printf("%T\n", s1i)
        //*main.S1
    
        // 因指向值的 pointer 可能為複數類型,所以 Go 無法反推其 pointer method
        // 如下
        // 究竟指向 s 的 pointer 是 Interface 還是 S1 呢?
        // 這決定是否合乎 Interface2 的接口,因 Interface 並無 test2()
        s := S1{} 
        Interface2(s) // S1 does not implement Interface2 (test method has pointer receiver)
    }
    
    或者說,事實上 interface 並不是 addressable 的
    所以當呼叫 pointer 的 method 是會出錯的
    因為無法傳地址過去給 method
    試想以下情況
    建立一個 A 並把它存在 I
    type I interface{}
    type A int
    type B string
    
    func main() {
        var a A = 5
        var i I = a
        fmt.Printf("i is of type %T\n", i)
    
        var aPtr *A
        aPtr = &(i.(A)) // 這是不被允許的語法,但在此假設如果能拿到 address
    
        // 然後建立一個 B 放在 i 中
        var b B = "hello"
        i = b
        fmt.Printf("i is of type %T, aPtr is of type %T\n", i, aPtr)
    
        // 理論上 output
        // i is of type main.A
        // i is of type main.B, aPtr is of type *main.A
        // 但在此出現矛盾了,aPtr 的 type 是 *main.A,但它卻指向了 main.B ??
        // interface 是動態決定類型,所以 interface 是不能 addressable 的
    }
    

Error
  • Go 主要是透過返回值來處理錯誤,並沒有一般語言的異常處理
  • Go 有 panic 和 recover function,不過它們很少使用
    panic 類似於拋出異常,recover 類似於 catch
  • 內建 error 格式
    type error interface {
        Error() string
    }
    
  • 使用方法
    package main
    
    import (
        "errors"
        "fmt"
        "time"
    )
    
    func process(count int) error {
        if count < 1 {
            // 使用內建的 error
            return errors.New("Invalid count")
        }
        return nil
    }
    
    // 自定錯誤,只要符合內建的 error 格式
    type MyError struct {
        When time.Time
        What string
    }
    
    func (e *MyError) Error() string {
        return fmt.Sprintf("at %v, %s",
            e.When, e.What)
    }
    
    func run() error {
        return &MyError{
            time.Now(),
            "it didn't work",
        }
    }
    
    func main() {
        fmt.Println(process(0)) // 內建錯誤
        fmt.Println(run())      // 自定錯誤
    }
    

Defer
  • function 返回前執行
  • 呼叫與執行 defer 採取的是 LIFO,function 中最後一個 defer 將優先執行
  • 變數的傳遞,會在呼叫 defer 的時候便直接傳入,後續變數有更正也不會修改
  • 使用方法,可用在讀檔後的關閉檔案,以免忘記
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        file, err := os.Open("a_file_to_read")
        if err != nil {
            fmt.Println(err)
            return
        }
        defer file.Close()
        // 讀檔
    }
    
    package main
    
    import (
        "errors"
        "fmt"
    )
    
    func useAClosure() error {
        var err error
        defer func() {
            fmt.Printf("useAClosure: err has value %v\n", err)
        }()
    
        err = errors.New("Error 1")
        fmt.Println("Finish func useAClosure")
    
        return err
    }
    
    func deferPrintf() error {
        var err error
        // 呼叫的時候會把參數帶過去,所以 err 是 nil
        defer fmt.Printf("deferPrintf: err has value %v\n", err)
    
        err = errors.New("Error 2")
    
        // 注意這裡是 LIFO,所以呼叫會是 43210
        for i := 0; i < 5; i++ {
            defer fmt.Printf("%d ", i)
        }
        fmt.Println("Finish func deferPrintf")
        return err
    }
    
    func main() {
        useAClosure()
        deferPrintf()
    }
    

Concurrency (並行)
  • Goroutine
    • goroutine 有點類似於執行緒,但它是由 Go 自己來調度安排的,而不是由作業系統
    • 主程式不會等到所有 goroutine 執行完才結束
    • 使用方法
      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          fmt.Println("start")
          go process()
          go process()
          go process()
          time.Sleep(time.Millisecond * 10) // 這是不好的方式,請別這麼做!
          fmt.Println("done")
      }
      
      func process() {
          fmt.Println("processing")
      }
      
    • 同步
      • 在並行化的程式中,唯一安全的方式是讀取變數
      • 可以有很多程式去讀一個變數,但寫入變數必須是同步的
      • 別因為加了鎖,反而從開在一個十線道的,突然轉往一個單行道
      • 小心 dead lock
        例如,當 goroutineA 有鎖 A,但它想要存取鎖 B,而 goroutineB 擁有鎖 B,但它想要存取鎖 A,結果互相鎖住了
      • 簡單 lock 範例,但不建議,因只是個單行道,並無改進效率
        package main
        
        import (
            "fmt"
            "sync"
            "time"
        )
        
        var (
            counter = 0
            lock    sync.Mutex
        )
        
        func main() {
            for i := 0; i < 20; i++ {
                go incr()
            }
            time.Sleep(time.Millisecond * 10) // 不夠精準,不好的寫法
        }
        
        func incr() {
            lock.Lock()
            defer lock.Unlock()
            counter++
            fmt.Println(counter)
        }
        
  • Channel
    • 對於簡單的例子,應使用 sync.Mutex 和 sync.RWMutex
    • 一個 Channel 就是不同的 goroutine 之間用來傳遞資料溝通的管道
    • 默認情況下,在另一端準備好之前,發送和接收都會阻塞,除非使用 select
    • 傳送
      CHANNEL <- DATA
    • 接收
      VAR := <-CHANNEL
    • 關閉
      close(CHANNEL)
    • 使用方法
      package main
      
      import "fmt"
      
      func sum(a []int, c chan int) {
          sum := 0
          for _, v := range a {
              sum += v
          }
          c <- sum // send sum to c
      }
      
      func main() {
          a := []int{7, 2, 8, -9, 4, 0}
      
          c := make(chan int)
          go sum(a[:len(a)/2], c)
          go sum(a[len(a)/2:], c)
          x, y := <-c, <-c // receive from c
      
          fmt.Println(x, y, x+y)
      }
    • 可以指定只能傳送 或 只能接收,宣告時加上箭頭
      package main
      
      import (
          "fmt"
          "time"
      )
      
      func send(c chan<- int) {
          c <- 1
      }
      
      func receive(c <-chan int) {
         fmt.Println(<-c)
      }
      
      func main() {
         c := make(chan int)
         go send(c)
         go receive(c)
         time.Sleep(time.Millisecond * 50)
      }
      
    • buffer
      • 改為 buffer 緩衝,不至於一開始就阻塞
        package main
        
        import (
            "fmt"
            "math/rand"
            "time"
        )
        
        func main() {
            // 建立暫存
            c := make(chan int, 100)
            for i := 0; i < 5; i++ {
                worker := &Worker{id: i}
                go worker.process(c)
            }
        
            for {
                c <- rand.Int()
                // 因來不及處理,即使有 buffer,還是會達到上限
                fmt.Println(len(c))
                time.Sleep(time.Millisecond * 50)
            }
        }
        
        type Worker struct {
            id int
        }
        
        func (w *Worker) process(c chan int) {
            for {
                data := <-c
                fmt.Printf("worker %d got %d\n", w.id, data)
                // 讓程式來不及處理傳送來的數據
                time.Sleep(time.Millisecond * 5000)
            }
        }
        
    • Close
      • 利用 close 終止 goroutine
        package main
        
        import (
            "fmt"
            "runtime/debug"
        )
        
        func main() {
            jobs := make(chan int, 5)
            done := make(chan bool)
        
            go func() {
                for {
                    // channel 已被 close 且所有值都已被接收,則 more 變為 false
                    j, more := <-jobs
                    if more {
                        fmt.Println("received job", j)
                    } else {
                        fmt.Println("received all jobs")
                        done <- true
                        return
                    }
                }
            }()
        
            for j := 1; j <= 3; j++ {
                fmt.Println("sent job", j)
                jobs <- j
            }
            // close channel 不再傳送
            close(jobs)
            fmt.Println("sent all jobs")
        
            <-done
        
            // 偵測是否有未關閉的 goroutine
            debug.SetTraceback("all")
            panic(1)
        }
        
    • 搭配 Select (丟棄資料)
      • 無 channel 可用時
        • 無 default,持續等待直到有一個可用的 channel
        • 有 default ,直接執行 default 下的程式碼
      • 多個 channel 可用時,select 會隨機選擇一個 channel
      • 使用方法
        package main
        
        import (
            "fmt"
            "time"
        )
        
        func after(d time.Duration) chan bool {
            c := make(chan bool)
            go func() {
                time.Sleep(d)
                c <- true
            }()
            return c
        }
        
        func main() {
            c := make(chan int)
            for i := 0; i < 5; i++ {
                worker := &Worker{id: i}
                go worker.process(c)
            }
            i := 0
            for {
                select {
                case c <- i:
                case <-after(time.Millisecond * 100):
                    // 可用 time.After(time.Millisecond * 100) 取代
                    // 逾時丟掉資料
                    fmt.Println("timed out,drop out", i)
                }
                time.Sleep(time.Millisecond * 50)
                i++
            }
        }
        
        type Worker struct {
            id int
        }
        
        func (w *Worker) process(c chan int) {
            for {
                data := <-c
                fmt.Printf("worker %d got %d\n", w.id, data)
                // 讓程式來不及處理傳送來的數據
                time.Sleep(time.Millisecond * 5000)
            }
        }
        

參考

1.3 Go 命令
初學 Golang 30 天(三)變數
[Golang] 相當好用但又要注意的defer
Golang- import 导入包的语法
How to use interfaces in Go Why value stored in an interface is not addressable in Golang
Go Data Structures: Interfaces

留言