Go语言通道(channel)

通道(channel)是Go语言中goroutine之间通信的主要方式。它类似于一个管道,允许一个goroutine向通道发送值,另一个goroutine从通道接收值。channel是类型化的,即只能传递特定类型的数据。通过channel,可以实现goroutine间的同步和数据共享,遵循“不要通过共享内存来通信,而要通过通信来共享内存”的设计哲学。

核心理念:通道是并发安全的,无需额外加锁。无缓冲通道提供同步保证;有缓冲通道提供异步能力。

1. 创建与基本操作

使用make(chan 类型)创建无缓冲通道,使用make(chan 类型, 容量)创建有缓冲通道。发送:ch <- value;接收:<-ch。

无缓冲通道示例
package main

import "fmt"

func main() {
    // 创建int类型的无缓冲通道
    ch := make(chan int)

    // 启动goroutine发送数据
    go func() {
        ch <- 42   // 发送
    }()

    // 主goroutine接收数据
    value := <-ch
    fmt.Println(value) // 42
}

发送和接收在双方都准备好之前会阻塞,因此无需额外等待机制。

2. 无缓冲通道(同步)

无缓冲通道的发送操作会阻塞,直到另一个goroutine执行接收操作。这使得无缓冲通道成为一个天然的同步点。

func worker(done chan bool) {
    fmt.Print("working...")
    // 模拟工作
    done <- true  // 发送完成信号
}

func main() {
    done := make(chan bool)
    go worker(done)
    <-done  // 等待worker完成
    fmt.Println(" done")
}

3. 有缓冲通道

有缓冲通道具备一个容量,发送操作只有在缓冲区满时才阻塞,接收操作只有在缓冲区空时才阻塞。

缓冲示例
func main() {
    ch := make(chan string, 2)
    ch <- "hello"
    ch <- "world"
    // ch <- "extra" // 会导致死锁,缓冲区已满

    fmt.Println(<-ch) // hello
    fmt.Println(<-ch) // world
}
有缓冲通道适用于允许发送方和接收方节奏不一致的场景,但过度缓冲可能掩盖并发问题。

4. 关闭通道与 for range

内置close函数可以关闭通道,表示不再有数据发送。接收方可以通过多返回值判断通道是否关闭。

func producer(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

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

    // 方式1:显式判断
    for {
        v, ok := <-ch
        if !ok {
            break
        }
        fmt.Println(v)
    }

    // 方式2:range自动判断关闭
    // for v := range ch {
    //     fmt.Println(v)
    // }
}
向已关闭的通道发送数据会导致panic;从已关闭的通道接收数据会立即返回零值(且ok为false)。关闭通道是发送方的责任,接收方不应关闭。

5. 单向通道

可以将通道声明为只发送或只接收,增加类型安全性。在函数参数中常用。

// 只发送通道:chan<- int
func sendOnly(ch chan<- int, value int) {
    ch <- value
    // <-ch // 编译错误
}

// 只接收通道:<-chan int
func receiveOnly(ch <-chan int) int {
    return <-ch
}

6. select 语句

select用于监听多个通道,哪个通道准备好就执行对应的case。常用于超时控制、非阻塞通信、多路复用等。

select基本用法
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "来自ch1"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "来自ch2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

6.1 超时控制

select {
case res := <-ch:
    fmt.Println(res)
case <-time.After(2 * time.Second):
    fmt.Println("超时,未收到数据")
}

6.2 非阻塞收发(default)

select {
case ch <- value:
    fmt.Println("发送成功")
case v := <-ch:
    fmt.Println("接收到", v)
default:
    fmt.Println("通道未就绪,不阻塞")
}

7. 生产者-消费者模式

使用有缓冲通道实现
func producer(ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 1; i <= 10; i++ {
        ch <- i
        fmt.Printf("生产: %d\n", i)
    }
    close(ch)
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for v := range ch {
        fmt.Printf("消费: %d\n", v)
    }
}

func main() {
    ch := make(chan int, 3)
    var wg sync.WaitGroup
    wg.Add(2)
    go producer(ch, &wg)
    go consumer(ch, &wg)
    wg.Wait()
}

8. 常见死锁场景

  • 无缓冲通道发送后没有接收者:在同一个goroutine中发送并等待接收会导致死锁。
  • 有缓冲通道缓冲区满且无接收者:继续发送会阻塞。
  • 通道未关闭且没有数据,接收方阻塞:通常配合range或select超时。
// 死锁示例:主goroutine自己发送并等待接收
func main() {
    ch := make(chan int)
    ch <- 1   // 阻塞,因为没有其他goroutine接收
    <-ch
}

综合示例:工作池

使用通道实现有限并发
package main

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

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("worker %d 处理任务 %d\n", id, job)
        time.Sleep(time.Millisecond * 200)
        results <- job * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    var wg sync.WaitGroup
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // 发送任务
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // 等待所有worker完成
    wg.Wait()
    close(results)

    // 收集结果
    for res := range results {
        fmt.Println("结果:", res)
    }
}
worker 3 处理任务 1
worker 1 处理任务 2
...
结果: 2
结果: 4
...
小结:通道是Go并发编程的核心元素,通过通道可以安全地交换数据并同步goroutine。无缓冲通道强调同步,有缓冲通道解耦生产消费。select使得多路复用成为可能。掌握channel后,可以构建高效、优雅的并发程序。下一章将介绍同步原语(Mutex、RWMutex)等,用于更细粒度的并发控制。