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)
    }
}

journey
32 声望22 粉丝