博客 / 詳情

返回

go chan 使用經驗分享

1、帶緩衝 vs 無緩存

1.1、帶緩衝

ch := make(chan int, num)

描述:這是一個 帶緩衝 的通道,緩衝區大小為 1

特性 :

  1. 發送數據到通道時,如果緩衝區未滿,發送操作不會阻塞
  2. 接收數據時,如果緩衝區不為空,接收操作不會阻塞
  3. 緩衝區的大小決定了可以在通道中存儲多少數據而不需要立即被接收

示例 :

ch := make(chan int, 1)
ch <- 42  // 不會阻塞,因為緩衝區可以容納一個值
fmt.Println(<-ch) // 從通道接收數據

1.2、無緩衝

ch := make(chan int)

描述:這是一個 無緩衝 的通道

特性 :

  1. 發送操作阻塞:只有當有接收方從通道中接收數據時,發送操作才能完成
  2. 接收操作阻塞:只有當有發送方向通道發送數據時,接收操作才能完成
  3. 由於沒有緩衝區,發送和接收必須同步完成

示例 :

ch := make(chan int)
go func() {
    ch <- 42 // 阻塞,直到有接收方接收
}()
fmt.Println(<-ch) // 接收數據並解除發送阻塞

2、經典使用場景

2.1、Goroutine 同步:等待任務完成

使用無緩衝通道同步多個 goroutine 的執行

package main

import (
    "fmt"
    "time"
)

func worker(done chan bool) {
    fmt.Println("開始工作...")
    time.Sleep(2 * time.Second)
    fmt.Println("工作完成")
    done <- true // 通知主線程工作完成
}

func main() {
    done := make(chan bool)

    go worker(done)

    <-done // 等待通知
    fmt.Println("所有工作已完成")
}

WaitGroup 同樣也可以實現以上的功能,類似Java中的CountDownLatch

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    w := sync.WaitGroup{}

    w.Add(1)
    go func() {
        time.Sleep(2 * time.Second)
        fmt.Println("工作完成")
        defer w.Done()
    }()

    w.Wait()

    fmt.Println("結束。。。。")

}

2.2、生產者-消費者模式

通過緩衝通道實現生產者和消費者的協作

package main

import (
    "fmt"
    "time"
)

func producer(ch chan int) {
    for i := 1; i <= 5; i++ {
        fmt.Println("生產:", i)
        ch <- i
        time.Sleep(1 * time.Second)
    }
    close(ch) // 關閉通道,通知消費者生產結束
}

func consumer(ch chan int) {
    for item := range ch { // 使用 range 自動判斷通道關閉
        fmt.Println("消費:", item)
    }
}

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

    go producer(ch)
    consumer(ch)
}

2.3、扇出模式(Fan-Out)

將一個任務分發到多個 goroutine 中並行處理

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d 正在處理任務 %d\n", id, job)
        time.Sleep(2 * time.Second)
        results <- job * 2 // 模擬結果
    }
}

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

    // 啓動 3 個 worker
    for i := 1; i <= 3; i++ {
        go worker(i, jobs, results)
    }

    // 發送任務
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // 關閉任務通道,通知 worker 任務完成

    // 接收結果
    for i := 1; i <= 5; i++ {
        fmt.Println("結果:", <-results)
    }
}

2.4、扇入模式(Fan-In)

將多個通道的數據匯聚到一個通道中

package main

import (
    "fmt"
    "time"
)

func generate(msg string, ch chan string) {
    for i := 0; i < 3; i++ {
        ch <- fmt.Sprintf("%s %d", msg, i)
        time.Sleep(1 * time.Second)
    }
    close(ch)
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go generate("通道1", ch1)
    go generate("通道2", ch2)

    done := make(chan bool)

    // 扇入: 從兩個通道中讀取數據並輸出
    go func() {
        for ch1 != nil || ch2 != nil {
            select {
            case msg1, ok := <-ch1:
                if ok {
                    fmt.Println("收到:", msg1)
                } else {
                    ch1 = nil
                }
            case msg2, ok := <-ch2:
                if ok {
                    fmt.Println("收到:", msg2)
                } else {
                    ch2 = nil
                }
            }
        }
        done <- true
    }()

    <-done // 等待完成
    fmt.Println("所有通道數據接收完畢")
}

2.5、超時控制

使用 select 和 time.After 實現超時機制

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- 42
    }()

    select {
    case res := <-ch:
        fmt.Println("收到結果:", res)
    case <-time.After(2 * time.Second):
        fmt.Println("超時了")
    }
}

2.6、限制併發數量

使用緩衝通道限制同時運行的 goroutine 數量

package main

import (
    "fmt"
    "time"
)

func worker(id int, sem chan struct{}) {
    sem <- struct{}{} // 佔用一個槽位
    fmt.Printf("Worker %d 開始\n", id)
    time.Sleep(2 * time.Second)
    fmt.Printf("Worker %d 完成\n", id)
    <-sem // 釋放槽位
}

func main() {
    const maxWorkers = 3
    sem := make(chan struct{}, maxWorkers)

    for i := 1; i <= 10; i++ {
        go worker(i, sem)
    }

    time.Sleep(10 * time.Second) // 等待所有工作完成
}

2.7、廣播通知

多個 goroutine 等待一個通道上的廣播信號

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, ch <-chan struct{}, wg *sync.WaitGroup) {
    defer wg.Done()
    <-ch // 等待廣播信號
    fmt.Printf("Worker %d 開始工作\n", id)
}

func main() {
    var wg sync.WaitGroup
    broadcast := make(chan struct{})

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, broadcast, &wg)
    }

    time.Sleep(2 * time.Second)
    close(broadcast) // 廣播信號,通知所有 worker
    wg.Wait()        // 等待所有 worker 完成
    fmt.Println("所有工作完成")
}

3、常見死鎖場景

3.1、無緩衝通道發送或接收未匹配

無緩衝通道需要發送方和接收方同步操作,否則會導致死鎖

死鎖代碼:

func main() {
    ch := make(chan int)
    ch <- 42 // 阻塞等待接收方,但沒有接收方
    fmt.Println(<-ch)
}

解決方法:
啓用 goroutine 來接收數據,確保發送和接收匹配

go func() {
    ch <- 42 // 在 goroutine 中發送數據
}()
fmt.Println(<-ch)

3.2、通道關閉後繼續發送數據

向已關閉的通道發送數據會導致運行時錯誤

func main() {
    ch := make(chan int, 1)
    close(ch)
    ch <- 42 // panic: send on closed channel
}

解決方法:
確保只關閉通道一次,並且關閉後不再發送數據

func main() {
    ch := make(chan int, 1)
    go func() {
        ch <- 42
        close(ch)
    }()
    for val := range ch {
        fmt.Println(val)
    }
}

3.3、所有 goroutine 都在等待

所有 goroutine 都在等待通道操作完成,形成死鎖

死鎖代碼

func main() {
    ch := make(chan int)
    <-ch // 主線程阻塞,沒有數據寫入通道
}

解決方法
確保至少一個 goroutine 能完成發送或接收操作

func main() {
    ch := make(chan int)
    go func() {
        ch <- 42 // goroutine 寫入數據
    }()
    fmt.Println(<-ch)
}

3.4、避免死鎖的最佳實踐

(1) 合理使用緩衝通道
緩衝通道可以存儲一定數量的數據,減少阻塞風險

func main() {
    ch := make(chan int, 2)
    ch <- 1 // 不阻塞
    ch <- 2 // 不阻塞
    fmt.Println(<-ch) // 讀取數據
    fmt.Println(<-ch)
}

(2) 使用 select 語句避免阻塞
select 允許在多通道之間選擇,避免因單個通道操作而死鎖

func main() {
    ch := make(chan int)
    done := make(chan bool)

    go func() {
        time.Sleep(1 * time.Second)
        ch <- 42
        done <- true
    }()

    select {
    case val := <-ch:
        fmt.Println("接收到數據:", val)
    case <-time.After(2 * time.Second): // 超時控制
        fmt.Println("操作超時")
    }
}

(3) 確保通道關閉和讀取完成
關閉通道通知接收者數據已發送完畢,可以使用 range 安全讀取

func main() {
    ch := make(chan int)

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

    for val := range ch { // 使用 range 讀取數據,直到通道關閉
        fmt.Println(val)
    }
}

(4) 避免過早關閉通道
錯誤代碼

func main() {
    ch := make(chan int)
    go func() {
        ch <- 42
    }()
    close(ch) // 過早關閉,可能導致 panic
    fmt.Println(<-ch)
}

正確做法

func main() {
    ch := make(chan int)
    go func() {
        ch <- 42
        close(ch) // 發送方負責關閉通道
    }()
    fmt.Println(<-ch)
}

(5) 使用 WaitGroup 管理併發
當多個 goroutine 協同工作時,使用 sync.WaitGroup 確保所有 goroutine 正常退出

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    ch := make(chan int, 5)

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            ch <- id
        }(i)
    }

    go func() {
        wg.Wait()
        close(ch) // 等待所有發送完成後關閉通道
    }()

    for val := range ch {
        fmt.Println("接收:", val)
    }
}
user avatar goudantiezhuerzi 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.