天天看點

讀Go語言精進之路

主要是摘取書中,個人感覺比較重要的内容。

文章目錄

    • 第一部分 熟知Go的一切
      • 了解Go的設計哲學
      • 使用Go語言原生程式設計思維寫Go代碼
    • 第二部分 項目結構、代碼風格和辨別符命名
    • 第三部分 聲明、類型、語句與控制結構
      • 13 了解切片的底層原理
      • 14 了解Map實作原理并高效使用
      • 15. string類型
      • 16. 了解Go語言的包導入
      • 17 了解Go語言表達式的求值順序
      • 19 了解控制語句慣用法
    • 第四部分 函數和方法
      • 20. 包級别的init函數
      • 21. 讓自己習慣于函數是一等公民
      • 22. 使用defer讓函數更簡單更健壯
      • 23 了解方法的本質以及選擇正确的receiver類型
      • 24 方法集合決定接口實作
      • 25. 變長參數函數的妙用
    • 第五部分 接口
      • 26 了解接口類型變量的内部表示
      • 27 盡量定義小接口
      • 29 使用接口作為程式水準組合的連接配接點
    • 第六部分 并發程式設計
      • 31 優先考慮并發設計
      • 32 了解goroutine的排程原理
      • 33 掌握Go并發模型和常見的并發模式
      • 34 Channel 的了解
      • 35 了解sync包的正确用法
    • 第七部分 錯誤處理
      • 37 了解錯誤處理的4種政策
      • 38 盡量優化反複出現的if err != nil
      • 39 不要使用panic進行正常的錯誤處理

第一部分 熟知Go的一切

了解Go的設計哲學

  1. 追去簡單,少就是多。Go設計者推崇最簡單方式思維,事情僅有一種或者盡可能少的方式去完成。Go的複雜性被go設計者所隐藏。比方:通過大小寫來說明是不是要暴露接口。
  2. 水準和垂直組合。接口與實作之間隐士關聯,包之間是互相獨立的,沒有子包概念。通過類型嵌入,快速讓一個新類型複用其他類型已經實作的能力,實作功能垂直擴充。這似乎是其他語言沒有實作的功能點。例子:
// $GOROOT/src/sync/pool.go
type poolLocal struct {
    private interface{}
    shared  []interface{}
    Mutex
    pad     [128]byte
}
           

poolLocal中嵌入了Mutex。所有poolLocal有了Mutex的Lock和Unlock方法,在實際調用中,方法調用會被傳給poolLocal的Mutex執行個體。通過Interface實作水準組合,将程式各個部分組合在一起。通過Interface将程式各個部分組合在一起的方式,筆者稱為水準組合。

3. 原生并發。面向多核的。傳統的多線程是,作業系統對程序,線程進行排程,是以消耗資源多。而go是實作了自己一套goroutine,然後對作業系統而言隻是排程了go,至于如何多線程是go關心的事情,go實作了自己的一套goroutine排程器。并發是将一個程式分解成多個小片段并且每個小片段都可以獨立執行的程式設計方法。

4. 面向工程,自帶電池。故意不支援預設函數參數,因為預設函數參數,會降低清晰度和可讀性。并且标準庫功能豐富,多數功能無需依賴第三方包。

使用Go語言原生程式設計思維寫Go代碼

不能影響到你的程式設計思維方式的程式設計語言不值得學習和使用。Go語言的程式設計思維,會随着本書的閱讀,越發清晰。

第二部分 項目結構、代碼風格和辨別符命名

  • 使用gofmt 對代碼進行format。
  • Go官方要求辨別符命名采用駝峰命名法。變量名不要帶類型資訊。
  • 循環和條件變量多采用單個字母命名;
  • 函數/方法的參數和傳回值變量以單個單詞或單個字母為主;
  • 由于方法在調用時會綁定類型資訊,是以方法的命名以單個單詞為主;
  • 函數多以多單詞的複合詞進行命名;類型多以多單詞的複合詞進行命名。
  • 常量通常用大寫的單詞命名。
  • 接口一般是 方法名 + er的命名

第三部分 聲明、類型、語句與控制結構

  1. 使用iota實作枚舉常量。
  2. 盡量定義零值可用的類型。Go語言每個原生類型都有預設值。整型:0,浮點類型:0.0,布爾: false,字元串: “”,指針,interface,slice, channel,map,function: nil。

13 了解切片的底層原理

傳遞數組是指拷貝,切片之于數組,就像是檔案描述符之于檔案。Go語言中,數組承擔的是底層存儲空間的角色,切片則是為底層的數組打開了一個通路的視窗。是以,切片是數組的“描述符”。

切片的内部表述:

//$GOROOT/src/runtime/slice.go
type slice struct {
    array unsafe.Pointer // 指向下層數組的某元素的指針,該元素也是切片的起始元素。
    len   int // 切片的長度
    cap   int // 切片的最大容量
}
           

假設建立一個切片執行個體: s:= make([]byte, 5)。運作時層面的内部表示如下:

讀Go語言精進之路

當切片作為函數參數傳遞給函數時,實際傳遞的是切片的内部表示,也就是runtime.slice結構,是以節省空間,沒有數組的拷貝。

另外切片還有動态擴容的特性。動态擴容帶來的是性能損耗,需要複制數組。是以,如果可以預估切片底層數組需要承載的元素數量,強烈建議在建立切片時帶上cap參數。

14 了解Map實作原理并高效使用

Map不支援零值可用。Map也是引用類型,将map類型變量作為函數參數傳入不會有很大的性能損耗。

最佳實踐是總是使用comma ok 慣用法來讀取map中的值。

m := map[string]int
v, ok := m["key"]
if !ok {
	// key 不在map中
}
fmt.Println(v)
// 删除資料
delete(m, "key2") // 即使删除的資料在map中不存在,delete也不會導緻panic
           

map不支援并發讀寫。并且盡量使用cap參數類建立map。

15. string類型

string類型底層存儲。

讀Go語言精進之路
// $GOROOT/src/runtime/string.go
type stringStruct struct {
    str unsafe.Pointer
    len int
}
           

sring類型是一個描述符,本身并不真正存儲資料,而是由一個指向底層存儲的指針和字元串長度組成。

string類型通過函數/方法參數傳入也不會有太多的損耗,因為傳入的僅僅是一個“描述符”,而不是真正的字元串資料。

byte string 互相轉換:

s := "haofan"
byteS := []byte(s)
h := string(byteS)
           

16. 了解Go語言的包導入

  1. Go 程式建構過程。先編譯成.a檔案,再把.a檔案,連結成.o檔案。
  2. Go包導入語句中import後面的部分是一個路徑,路徑的最後一個分段是目錄名而不是包名。

17 了解Go語言表達式的求值順序

  1. 包級别變了初始化,按照變量聲明先後順序進行,a = b + c ,則是從c開始算。
  2. Go規定表達式操作數中的所有函數、方法以及channel操作從左到右的次序進行求值。
  3. 另外,指派語句求值,是按照從左到右的順序對變量進行指派。

19 了解控制語句慣用法

  1. 使用if語句遵循快樂路徑原則。所謂,快樂路徑:
    • 出現錯誤時,快速傳回。成功邏輯,不要嵌入if-else中,傳回值一般在函數最後一行。
  2. for range 閉坑。用短變量 := 指派,
  3. 參與疊代的是range表達式的副本,是以如果在range語句内,修改數組值,則原數組不變。如果要修改數組,則需要用數組指針。
// chapter3/sources/control_structure_idiom_2.go
...
func pointerToArrayRangeExpression() {
   var a = [5]int{1, 2, 3, 4, 5}
   var r [5]int

   fmt.Println("pointerToArrayRangeExpression result:")
   fmt.Println("a = ", a)

   for i, v := range &a {
       if i == 0 {
           a[1] = 12
           a[2] = 13
       }
       r[i] = v
   }

   fmt.Println("r = ", r)
   fmt.Println("a = ", a)
}
           
  1. break 語句跳到哪裡。是跳到同一函數内break語句所在的最内層的for, switch或select的執行。go 是可以通過定義label的方式,指定break到哪裡?

第四部分 函數和方法

20. 包級别的init函數

main包中,Go運作時,會按照常量->變量->init函數的順序進行初始化。

init函數特點: 運作時調用、順序、僅執行一次。通過init函數的注冊模式:

import (
    "database/sql"
    _ "github.com/lib/pq"
)

func main() {
    db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full")
    if err != nil {
        log.Fatal(err)
    }

    age := 21
    rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
    ...
}
           

神奇的地方是,在import pg package後,似乎沒有地方調用,奧秘全在:

// github.com/lib/pq/conn.go
...

func init() {
    sql.Register("postgres", &Driver{})
}
...
           

pg包的Init函數執行後,pg包将自己實作的dirver注冊到sql包中。這樣,在應用層代碼在打開資料庫的時候傳入驅動的名字(postgress),通過sql.Open傳回的句柄,就是pg這個驅動的相應實作。

21. 讓自己習慣于函數是一等公民

如果一門程式設計語言對某種語言元素的建立和使用沒有限制,我們可以像對待value一樣對待這種文法元素,那麼我們就稱這種文法元素是這門程式設計語言的"一等公民"。

滿足的條件成為一等公民(像普通整型值那樣被建立和使用):

  • 在函數内建立,也就是在函數内定義一個新的函數。
  • 作為類型,使用函數來自定義類型。type HandlerFunc func(ReponseWriter, *Request)
  • 存儲到變量中。
  • 作為參數傳入函數。
  • 作為傳回值從函數傳回。

例子:

func greeting(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome, Gopher!\n")
}

func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

// $GOROOT/src/net/http/server.go
// HandlerFunc是一個機遇函數定義的新類型,它的底層類型為func(ResponseWriter, *Request),該類型有一個方法是ServeHTTP
// 因而實作了Handler接口。http.HandlerFunc(greeting)這句代碼的真正含義
// 是将函數greeting顯式轉換為HandlerFunc類型,而後者實作了Handler接口
// 這樣轉型後的greeting就滿足了ListenAndServe函數第二個參數的要求。
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP調用f(w, r)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

// 接口handler
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
           

22. 使用defer讓函數更簡單更健壯

按照後進先出的順序,排程執行defer

23 了解方法的本質以及選擇正确的receiver類型

方法和函數的差別是,方法需要有receiver。

Go方法的本質:一個以方法所綁定類型執行個體為第一個參數的普通函數。

如果沒有對類型執行個體修改的需求,那麼為receiver選擇T類型或T類型均可;但考慮到Go方法調用時,receiver是以值複制的形式傳入方法中的,如果類型的size較大,以值形式傳入會導緻較大損耗,這時選擇T作為receiver類型會更好些。

24 方法集合決定接口實作

Go語言的一個創新是,自定義類型與接口之間的實作關系是松耦合的。如果某個自定義類型T的方法集合是某個接口類型方法集合的超集,那麼就說類型T實作了該接口,并且類型T的變量可以被指派給該接口類型的變量,也就是方法集合決定接口實作。

類型T的方法集合是以T為receiver類型的所有方法的集合,類型T的方法集合是以T為receiver類型的所有方法的集合與類型T方法集合的并集;

25. 變長參數函數的妙用

什麼是變長參數函數:

func sum(args ...int) int {
	var total int
	for _, v := range args {
		total += v
	}
	return total
}
           

Go 語言不支援函數重載,那通過變長函數如何實作呢? 如下。同樣也可以通過變長函數實作預設參數,但是這種做法其實比較醜陋。

// chapter4/sources/variadic_function_5.go

func concat(sep string, args ...interface{}) string {
    var result string
    for i, v := range args {
        if i != 0 {
            result += sep
        }
        switch v.(type) {
        case int, int8, int16, int32, int64,
            uint, uint8, uint16, uint32, uint64:
            result += fmt.Sprintf("%d", v)
        case string:
            result += fmt.Sprintf("%s", v)
        case []int:
            ints := v.([]int)
            for i, v := range ints {
                if i != 0 {
                    result += sep
                }
                result += fmt.Sprintf("%d", v)
            }
        case []string:
            strs := v.([]string)
            result += strings.Join(strs, sep)
        default:
            fmt.Printf("the argument type [%T] is not supported", v)
            return ""
        }
    }
    return result
}

func main() {
    println(concat("-", 1, 2))
    println(concat("-", "hello", "gopher"))
    println(concat("-", "hello", 1, uint32(2),
        []int{11, 12, 13}, 17,
        []string{"robot", "ai", "ml"},
        "hacker", 33))
}
           

功能選項模式:

// chapter4/sources/variadic_function_9.go

type FinishedHouse struct {
    style                  int    // 0: Chinese; 1: American; 2: European
    centralAirConditioning bool   // true或false
    floorMaterial          string  // "ground-tile"或"wood"
    wallMaterial           string // "latex"或"paper"或"diatom-mud"
}

type Option func(*FinishedHouse)

func NewFinishedHouse(options ...Option) *FinishedHouse {
    h := &FinishedHouse{
        // default options
        style:                  0,
        centralAirConditioning: true,
        floorMaterial:          "wood",
        wallMaterial:           "paper",
    }

    for _, option := range options {
        option(h)
    }

    return h
}

func WithStyle(style int) Option {
    return func(h *FinishedHouse) {
        h.style = style
    }
}

func WithFloorMaterial(material string) Option {
    return func(h *FinishedHouse) {
        h.floorMaterial = material
    }
}

func WithWallMaterial(material string) Option {
    return func(h *FinishedHouse) {
        h.wallMaterial = material
    }
}

func WithCentralAirConditioning(centralAirConditioning bool) Option {
    return func(h *FinishedHouse) {
        h.centralAirConditioning = centralAirConditioning
    }
}

func main() {
    fmt.Printf("%+v\n", NewFinishedHouse()) // 使用預設選項
    fmt.Printf("%+v\n", NewFinishedHouse(WithStyle(1),
        WithFloorMaterial("ground-tile"),
        WithCentralAirConditioning(false)))
}
           

第五部分 接口

Go語言推崇面向組合程式設計,接口是組合程式設計的重要手段。接口是Go這門靜态類型語言中唯一“動靜兼備”的語言特性。

當一個接口類型變量被指派時,編譯器會檢查右值的類型是否實作了該接口方法集合中的所有方法。

接口類型變量在程式運作時可以被指派為不同的動态類型變量,進而支援運作時多态。

26 了解接口類型變量的内部表示

接口類型變量在運作時表示為eface和iface,eface用于表示空接口類型變量,iface用于表示非空接口類型變量;當且僅當兩個接口類型變量的類型資訊(eface._type/iface.tab._type)相同,且資料指針(eface.data/iface.data)所指資料相同時,兩個接口類型才是相等的;通過println可以輸出接口類型變量的兩部分指針變量的值;可通過複制runtime包eface和iface相關類型源碼,自定義輸出eface/iface詳盡資訊的函數;接口類型變量的裝箱操作由Go編譯器和運作時共同完成。

27 盡量定義小接口

Go語言中接口與實作之間是隐式的,實作者僅需實作接口方法集中的全部方法,便算是自動遵守了契約,實作了該接口。接口的方法數量盡量控制在1~3個。

小接口優勢:

  1. 接口越小,抽象度越高,被接納度越高
  2. 易于實作和測試。
  3. 契約職責單一,易于複用組合。

29 使用接口作為程式水準組合的連接配接點

如果說C++和Java是關于類型層次結構和類型分類的語言,那麼Go則是關于組合的語言。 — Rob Pike Go語言之父

Go的組合方式:

  • 垂直組合。Go通過類型嵌入機制實作垂直組合,進而實作方法實作的複用、接口定義重用等。
  • 水準組合。Go程式以接口類型變量作為程式水準組合的連接配接點。接口好比人體的關節,連接配接人體不同的部分。

以接口為連接配接點的水準組合的幾種形式:

  1. 基本形式。接受接口類型參數的函數和方法。

    func YourFuncName(param InterfaceType)

  2. 包裹函數。接受接口類型參數,并傳回與其參數類型相同的傳回值。

    func YourWrapperFunc(param InterfaceType) InterfaceType

    通過包裹函數可以實作對輸入資料的過濾,裝飾,變換等操作,并将結果再次傳回給調用者。

    例子:

// chapter5/sources/horizontal-composition-2.go

func CapReader(r io.Reader) io.Reader {
    return &capitalizedReader{r: r}
}

type capitalizedReader struct {
    r io.Reader
}

func (r *capitalizedReader) Read(p []byte) (int, error) {
    n, err := r.r.Read(p)
    if err != nil {
        return 0, err
    }

    q := bytes.ToUpper(p)
    for i, v := range q {
        p[i] = v
    }
    return n, err
}

func main() {
    r := strings.NewReader("hello, gopher!\n")
    r1 := CapReader(io.LimitReader(r, 4))
    if _, err := io.Copy(os.Stdout, r1); err != nil {
        log.Fatal(err)
    }
}
           
  1. 擴充卡函函數類型。
  2. 中間件。在Go web中,常常指的是一個實作了http.Handler接口的http.HandlerFunc類型執行個體。
// chapter5/sources/horizontal-composition-4.go

func validateAuth(s string) error {
    if s != "123456" {
        return fmt.Errorf("%s", "bad auth token")
    }
    return nil
}

func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}

func logHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t)
        h.ServeHTTP(w, r)
    })
}

func authHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        err := validateAuth(r.Header.Get("auth"))
        if err != nil {
            http.Error(w, "bad auth param", http.StatusUnauthorized)
            return
        }
        h.ServeHTTP(w, r)
    })
}

func main() {
    http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings))))
}
           

另外使用接口接口可以降低耦合,進而更好的友善代碼測試。 例子

第六部分 并發程式設計

31 優先考慮并發設計

并發不是并行,并發關乎結構,并行關乎執行 – Rob Pike

并發與并行差別:

  • 并行方案:處理器核數充足的情況下啟動多個單線程應用的執行個體,這樣每個執行個體運作在一個核上。這種方案是有限制的,對于不支援在同一環境下部署多執行個體。
  • 并發方案:并發是重新做應用結構設計,将應用分解成多個基本執行單元中執行。應用内部拆分成多個可獨立運作的子產品。這樣雖然應用仍然以單例方法運作,但其中内部子產品都運作一個單獨的作業系統線程中,多核資源得以充分利用。
    讀Go語言精進之路

傳統程式設計語言,是基于多線程模型設計的,是以作業系統線程作為承載分解後的代碼片段的執行單元,由作業系統執行排程。

而Go語言并未使用作業系統線程作為承載分解後的代碼片段的基本執行單元,而是實作了goroutine這一由go運作時負責排程的用于層輕量級線程提供原生支援。goroutine相比傳統作業系統線程的優勢:

  1. 資源占用小。每個goroutine初始棧大小僅為2KB。
  2. Goroutine上下文切換代價小。
  3. 語言原生支援。又go關鍵字。
  4. 語言内置channel,作為goroutine通信機制。

因為Go是面向并發而生的,應用設計階段,優先考慮并發。

典型例子: link

32 了解goroutine的排程原理

Go運作時負責對goroutine進行排程,排程就是決定何時哪個goroutine将獲得資源開始執行,哪個goroutine應該停止執行讓出資源等。

GPM排程模型:

讀Go語言精進之路

其中,P是一個邏輯處理器,每個G要想真正運作起來,首先需要被配置設定一個P,即進入P的本地運作隊列中。對于G來說,P就是運作它的CPU。但從goroutine排程來說,真正的CPU是M,隻有将P和M綁定才能讓P本地運作隊列中的G真正運作起來。

  • G: 代表goroutine,存儲了goroutine的執行棧資訊,goroutine狀态以及任務函數等。
  • P:代表processor,p的數量決定了系統内最大可并行的G的數量。
  • M:代表真正的執行計算資源。在綁定有效的P後,進行一個排程循環。排程循環的機制大緻是從各種隊列、P的本地隊列中擷取G,然後執行G的函數。

問題:為什麼在死循環的情況下,多個goroutine依舊會被排程并輪流執行 ?

// chapter6/sources/go-scheduler-model-case1.go
func deadloop() {
    for {
    }
}

func main() {
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

$go run go-scheduler-model-case1.go
I got scheduled!
I got scheduled!
I got scheduled!
...
           

因為P的數量是CPU的核數,上面的例子啟動後,建立了兩個P,main goroutine一個P,for loop一個P,是以會執行。

33 掌握Go并發模型和常見的并發模式

傳統程式設計語言是基于共享記憶體的并發模型。Go語言是通過channel來實作。

Go的并發原語:

  • goroutine: 封裝了資料的處理邏輯,是go運作時排程的基本執行單元。
  • channel: 用于goroutine之間的通信和同步。
  • select: 用于應對多路輸入、輸出,可以讓goroutine同時協調處理多個channel操作

1 等待一個goroutine的退出, 例子。mian goroutine在建立完新的goroutine後,便在channel上阻塞等待,直到新goroutine退出前向該channel發送了一個信号。

2 等待多個goroutine的退出,例子。通過sync.WaitGroup方法

3 支援逾時機制的等待。通過timer

// chapter6/sources/go-concurrency-pattern-4.go
func main() {
    done := spawnGroup(5, worker, 30)
    println("spawn a group of workers")
    
    timer := time.NewTimer(time.Second * 5)
    defer timer.Stop()
    // 通過select原語同時監聽timer.C和done兩個channel,哪個先傳回資料就執行哪個case 分支
    select {
    case <-timer.C:
        println("wait group workers exit timeout!")
    case <-done:
        println("group workers done")
    }
}
           

34 Channel 的了解

channel是go的一等公民。我們可以像使用普通變量那樣使用channel。

c := make(chan int)    // 建立一個無緩沖(unbuffered)的int類型的channel
c := make(chan int, 5) // 建立一個帶緩沖的int類型的channel
c <- x        // 向channel c中發送一個值
<- c          // 從channel c中接收一個值
x = <- c      // 從channel c接收一個值并将其存儲到變量x中
x, ok = <- c  // 從channel c接收一個值。若channel關閉了,ok将被置為false
for i := range c { ... } // 将for range與channel結合使用
close(c)      // 關閉channel c

c := make(chan chan int) // 建立一個無緩沖的chan int類型的channel
func stream(ctx context.Context, out chan<- Value) error // 将隻發送(send-only) channel作為函數參數
func spawn(...) <-chan T // 将隻接收(receive-only)類型channel作為傳回值
           

當需同時對多個channel進行操作時,會使用select。通過select,可以同時在多個channel上進行發送、接收操作。

select {
    case x := <-c1: // 從channel c1接收資料
        ...
    case y, ok := <-c2: // 從channel c2接收資料,并根據ok值判斷c2是否已經關閉
        ...
    case c3 <- z: // 将z值發送到channel c3中
        ...
    default: // 當上面case中的channel通信均無法實施時,執行該預設分支
}
           

收發操作:

  1. channel的收發操作都是向左的箭頭

    (<-),data = <- chanX

    ,表示向從channel裡接收資料。

    chanX <- data

    表示發送資料到channel。
  2. 讀出操作可以是

    data := <-chanX 、 data = <-chanX 、 _ = <-chanX 、 <-chanX、 、 data, ok := <-chanX

Channel具有先進先出(FIFO)的性質,内部确定使用了循環隊列。

  • buffered channel,

    chanX := make(chan int, 3)

    . 當發送者向channel發送資料而接收者還沒有就緒時,如果buffer未滿,就會将資料放入buffer;
  • unbuffered channel,

    chanX := make(chan int)

    . 由于沒有暫存資料的地方,unbuffered channel的資料傳輸隻能是同步的,即隻有讀寫雙方都就緒時,通信才能成功。如果一方沒有ready,則會阻塞。
mychan := make(chan int)
go func() {
	fmt.Println("Send 100")
	mychan <- 100
	fmt.Println("Has sent")
}()
# 如果comment下面的代碼,則隻會輸出"Send 100",因為channel被阻塞了。
<-mychan
time.Sleep(time.Second * time.Duration(1))
           

發送步驟:

  1. 如果存在阻塞等待的接收者(goroutine),那麼直接将待發送的資料交給"等待接收隊列"中的第一個goroutine。
  2. 如果不存在。若buffer還有空間,則将待發送的資料送到buffer的隊尾。若buffer沒有空間,則将發送者(goroutine)和要發送的資料打包成一個struct,加入到等待發送隊列的隊尾,同時将該發送者block。

接收步驟:

3. 如果存在阻塞等待的發送者。若buffer已滿,從buffer中取出首元素交給接收者,同時将對應的goroutine喚醒。若沒有buffer,從等待發送隊列中取出對首元素,将要發送的資料copy給接收者,并将goroutine喚醒。

4. 如果沒有在阻塞等待的發送者。若buffer有資料,則取出首元素給接收者。若buffer空,那麼将接收者block。

for range 讀取select:

5. 如果發送端不是一直發資料,且沒有關閉channel,那麼,for range讀取會陷入block,道理很簡單,沒有資料可讀了。是以,要麼您能把控全局,確定您的for range讀取不會block;要麼,别用for range讀channel。

6. select不是loop,當它select了一個case執行後,整個select就結束了。是以,如果想要一直select,那就在select外層加上for吧。

35 了解sync包的正确用法

Go語言在提供CSP并發模型原語的同時,還通過sync包提供了傳統的基于共享記憶體并發模型的基本同步原語。包括sync.Mutex(互斥鎖),sync.RWMutex(讀寫鎖),sync.Cond(條件變量)。

可以使用sync.Once實作單例模式。

第七部分 錯誤處理

寫出高品質的Go代碼,我們需要始終想着錯誤處理。

37 了解錯誤處理的4種政策

構造錯誤值。任何實作了Error() string方法類型的執行個體均可作為錯誤複制給error 接口變量。

err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)
wrapErr = fmt.Errorf("wrap error: %w", err) // 僅Go 1.13及後續版本可用
           
  1. 透明錯誤處理政策。不關心傳回錯誤值攜帶的具體上下文資訊,隻要發生錯誤就進入唯一的錯誤處理執行路徑。80%都是這種錯誤處理政策。
err := doSomething()
if err != nil {
    // 不關心err變量底層錯誤值所攜帶的具體上下文資訊
    // 執行簡單錯誤處理邏輯并傳回
    ...
    return err
}
           
  1. 哨兵 錯誤處理政策
// $GOROOT/src/bufio/bufio.go
var (
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)

// 錯誤處理代碼
data, err := b.Peek(1)
if err != nil {
    switch err {
    case bufio.ErrNegativeCount:
        // ...
        return
    case bufio.ErrBufferFull:
        // ...
        return
    case bufio.ErrInvalidUnreadByte:
        // ...
        return
    default:
        // ...
        return
    }
}

// 或者

if err := doSomething(); err == bufio.ErrBufferFull {
    // 處理緩沖區滿的錯誤情況
    ...
}
           
  1. 錯誤值類型檢視政策
  2. 錯誤行為特征檢視政策

38 盡量優化反複出現的if err != nil

優化反複出現的if err != nil代碼塊的根本目的是讓錯誤檢查和處理較少,不要幹擾正常業務代碼,讓正常業務代碼更具視覺連續性。大緻有兩個努力的方向。

  1. 改善代碼的視覺呈現。
func SomeFunc() error {
    err := doStuff1()
    if err != nil {
        // 處理錯誤
    }
    
    err = doStuff2()
    if err != nil {
        // 處理錯誤
    }
    
    err = doStuff3()
    if err != nil {
        // 處理錯誤
    }
}
           
  1. 重構:減少if err != nil 的重複次數
  2. 内置error 狀态

39 不要使用panic進行正常的錯誤處理