GoLang 通道

📢 本文由 gemini-2.5-flash 翻譯

Golang 系列

Hello GoLang: https://blog.yexca.net/archives/154
GoLang (var and const) 變數與常數: https://blog.yexca.net/archives/155
GoLang (func) 函式: https://blog.yexca.net/archives/156
GoLang (slice and map) 切片: https://blog.yexca.net/archives/160
GoLang (OOP) 物件導向: https://blog.yexca.net/archives/162
GoLang (reflect) 反射: https://blog.yexca.net/archives/204
GoLang (struct tag) 結構體標籤: https://blog.yexca.net/archives/205
GoLang (goroutine) Go 協程: https://blog.yexca.net/archives/206
GoLang (channel) 通道: 本文


Go 協程 (goroutine) 可以透過通道 (channel) 傳遞資料,引用型別的通道可用於多個 Go 協程之間的通訊,其內部實作了同步機制,確保併發安全

有點類似 RabbitMQ (僅個人為方便學習所類比,實則為不同東西)

定義變數

通道為引用型別,複製或函式呼叫時將引用同一個通道物件,零值為 nil

透過 make() 函式建立,例如

1
2
3
c := make(chan int)
// 新增容量為 3
c := make(chan int, 3)

當容量為 0 時,通道是無緩衝區、會阻塞讀寫的;大於 0 時有緩衝區、非阻塞,直到寫滿才會阻塞

透過 <- 來接收和傳送資料

1
2
3
4
5
6
7
8
// 傳送資料到通道
channel <- 3
// 接收並捨棄
<-channel // 注意無空格
// 接收並賦值給變數
x := <-channel
// 接收並賦值給變數,並判斷是否接收成功(通道是否為空)
data, flag := <-channel

無緩衝區

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        defer fmt.Println("A.defer")
        c <- 6
        fmt.Println("A 正在執行")
    }()

    num := <-c
    fmt.Println("num =", num)

    fmt.Println("main 結束")
}

有緩衝區

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int, 3)

    go func() {
        defer fmt.Println("A 結束")
        for i := 0; i < 3; i++ {
            c <- i
            fmt.Println("A Go 協程, i =", i, "len =", len(c), "cap =", cap(c))
        }
    }()

    time.Sleep(2 * time.Second)

    for i := 0; i < 3; i++ {
        num := <-c
        fmt.Println("main, num =", num)
    }

    fmt.Println("main 結束")
}

關閉通道

透過 close() 關閉

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main() {
    c := make(chan int, 5)

    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        // 關閉
        close(c)
    }()

    for {
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }

    fmt.Println("Main 完成")
}

使用 range

上述 main 的 for 迴圈可以簡寫使用 range

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "fmt"

func main() {
    c := make(chan int, 5)

    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }

        close(c)
    }()

    //for {
    //  if data, ok := <-c; ok {
    //      fmt.Println(data)
    //  } else {
    //      break
    //  }
    //}

    for data := range c {
        fmt.Println(data)
    }
    fmt.Println("Main 完成")
}

單向通道

預設情況下,通道是雙向的,即可讀可寫,也可以指定通道方向,只讀或只寫

1
2
3
4
5
var c chan int // 宣告正常雙向通道
// c1 只可寫
var c1 chan<- int
// c2 只可讀
var c2 <-chan int

可以把雙向通道轉為單向,反之則不行。也就是可以定義函式形參為單向,但傳遞雙向通道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//   chan<- // 只寫
func counter(out chan<- int) {
    defer close(out)
    for i := 0; i < 5; i++ {
        out <- i // 如果對方不讀,會阻塞
    }
}
 
//   <-chan // 只讀
func printer(in <-chan int) {
    for num := range in {
        fmt.Println(num)
    }
}
 
func main() {
    c := make(chan int) // 雙向
 
    go counter(c) // 生產者
    printer(c)    // 消費者
 
    fmt.Println("完成")
}

select

select 可以監聽多個通道上的資料流動,語法與 switch 類似,但每個 case 敘述中必須是一個 I/O 操作

一般放到 for{} 敘述區塊中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

func main() {
    c := make(chan int)
    quit := make(chan int)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }

        quit <- 0
    }()

    // main
    x, y := 1, 1
    for {
        select {
        case c <- x:
            tmp := x
            x = y
            y = tmp + y
        case <-quit:
            fmt.Println("退出")
            return
         // 可以有 default,此範例不需要
        }
    }
}