[系列] Go - chan 通道

新亮筆記發表於2019-08-02

概述

原來分享基礎語法的時候,還未分享過 chan 通道,這次把它補上。

chan 可以理解為佇列,遵循先進先出的規則。

在說 chan 之前,我們們先說一下 go 關鍵字。

在 go 關鍵字後面加一個函式,就可以建立一個執行緒,函式可以為已經寫好的函式,也可以是匿名函式。

舉個例子:

func main() {
    fmt.Println("main start")

    go func() {
        fmt.Println("goroutine")
    }()

    fmt.Println("main end")
}

輸出:

main start
main end

為什麼沒有輸出 goroutine ?

首先,我們清楚 Go 語言的執行緒是併發機制,不是並行機制。

那麼,什麼是併發,什麼是並行?

併發是不同的程式碼塊交替執行,也就是交替可以做不同的事情。

並行是不同的程式碼塊同時執行,也就是同時可以做不同的事情。

舉個生活化場景的例子:

你正在家看書,忽然電話來了,然後你接電話,通話完成後繼續看書,這就是併發,看書和接電話交替做。

如果電話來了,你一邊看書一遍接電話,這就是並行,看書和接電話一起做。

說回上面的例子,為什麼沒有輸出 goroutine ?

main 函式是一個主執行緒,是因為主執行緒執行太快了,子執行緒還沒來得及執行,所以看不到輸出。

現在讓主執行緒休眠 1 秒鐘,再試試。

func main() {
    fmt.Println("main start")

    go func() {
        fmt.Println("goroutine")
    }()

    time.Sleep(1 * time.Second)

    fmt.Println("main end")
}

輸出:

main start
goroutine
main end

這就對了。

接下來,看看如何使用 chan 。

宣告 chan

// 宣告不帶緩衝的通道
ch1 := make(chan string)

// 宣告帶10個緩衝的通道
ch2 := make(chan string, 10)

// 宣告只讀通道
ch3 := make(<-chan string)

// 宣告只寫通道
ch4 := make(chan<- string)

注意:

不帶緩衝的通道,進和出都會阻塞。

帶緩衝的通道,進一次長度 +1,出一次長度 -1,如果長度等於緩衝長度時,再進就會阻塞。

寫入 chan

ch1 := make(chan string, 10)

ch1 <- "a"

讀取 chan

val, ok := <- ch1
// 或
val := <- ch1

關閉 chan

close(chan)

注意:

  • close 以後不能再寫入,寫入會出現 panic
  • 重複 close 會出現 panic
  • 只讀的 chan 不能 close
  • close 以後還可以讀取資料

示例

func main() {
    fmt.Println("main start")
    ch := make(chan string)
    ch <- "a" // 入 chan
    go func() {
        val := <- ch // 出 chan
        fmt.Println(val)
    }()
    fmt.Println("main end")
}

輸出:

main start
fatal error: all goroutines are asleep - deadlock!

What ? 這是為啥,剛開始就出師不利呀?

因為,定義的是一個無緩衝的 chan,賦值後就陷入了阻塞。

怎麼解決它?

宣告一個有緩衝的 chan。

func main() {
    fmt.Println("main start")
    ch := make(chan string, 1)
    ch <- "a" // 入 chan
    go func() {
        val := <- ch // 出 chan
        fmt.Println(val)
    }()
    fmt.Println("main end")
}

輸出:

main start
main end

為啥沒有輸出 a , 和前面一樣,主執行緒執行太快了,加個休眠 1 秒鐘,再試試。

func main() {
    fmt.Println("main start")
    ch := make(chan string, 1)
    ch <- "a" // 入 chan
    go func() {
        val := <- ch // 出 chan
        fmt.Println(val)
    }()
    time.Sleep(1 * time.Second)
    fmt.Println("main end")
}

輸出:

main start
a
main end

這就對了。

再看一個例子:

func main() {
    fmt.Println("main start")
    ch := make(chan string)
    go func() {
        ch <- "a" // 入 chan
    }()
    go func() {
        val := <- ch // 出 chan
        fmt.Println(val)
    }()
    time.Sleep(1 * time.Second)
    fmt.Println("main end")
}

輸出:

main start
a
main end

再看一個例子:

func producer(ch chan string) {
    fmt.Println("producer start")
    ch <- "a"
    ch <- "b"
    ch <- "c"
    ch <- "d"
    fmt.Println("producer end")
}

func main() {
    fmt.Println("main start")
    ch := make(chan string, 3)
    go producer(ch)

    time.Sleep(1 * time.Second)
    fmt.Println("main end")
}

輸出:

main start
producer start
main end

帶緩衝的通道,如果長度等於緩衝長度時,再進就會阻塞。

再看一個例子:

func producer(ch chan string) {
    fmt.Println("producer start")
    ch <- "a"
    ch <- "b"
    ch <- "c"
    ch <- "d"
    fmt.Println("producer end")
}

func customer(ch chan string) {
    for {
        msg := <- ch
        fmt.Println(msg)
    }
}

func main() {
    fmt.Println("main start")
    ch := make(chan string, 3)
    go producer(ch)
    go customer(ch)

    time.Sleep(1 * time.Second)
    fmt.Println("main end")
}

輸出:

main start
producer start
producer end
a
b
c
d
main end

就到這吧。

推薦閱讀

gRPC

Gin 框架

基礎篇

本文歡迎轉發,轉發請註明作者和出處,謝謝!

相關文章