目录

Go Channel

Channel 是 Go 并发编程中 goroutine 之间通信的核心机制。Go 的哲学是"不要通过共享内存来通信,而应通过通信来共享内存"。Channel 就像一个类型安全的管道,让 goroutine 之间可以安全地传递数据,无需手动加锁。

https://img.zhaojq.top/20260729163058662.png
Channel数据流

创建 channel

使用 make 函数创建 channel,chan 关键字声明 channel 类型。

package main

import "fmt"

func main() {
	// 创建无缓冲 channel
	ch1 := make(chan int)

	// 创建有缓冲 channel(容量为 10)
	ch2 := make(chan string, 10)

	fmt.Printf("ch1 类型: %T, 容量: %d\n", ch1, cap(ch1))
	fmt.Printf("ch2 类型: %T, 容量: %d\n", ch2, cap(ch2))
}

make(chan T) 创建无缓冲 channel,make(chan T, capacity) 创建有缓冲 channel。容量为 0 或省略时为无缓冲。

发送和接收

<- 操作符用于发送和接收数据。方向取决于它在 chan 的哪一侧。

package main

import (
	"fmt"
	"time"
)

func sender(ch chan int) {
	for i := 1; i <= 5; i++ {
		ch <- i // 发送到 channel
		fmt.Printf("发送: %d\n", i)
		time.Sleep(50 * time.Millisecond)
	}
	close(ch) // 发送完毕后关闭
}

func receiver(ch chan int, done chan bool) {
	for {
		val, ok := <-ch // 从 channel 接收
		if !ok {
			fmt.Println("channel 已关闭")
			done <- true
			return
		}
		fmt.Printf("接收: %d\n", val)
	}
}

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

	go sender(ch)
	go receiver(ch, done)

	<-done // 等待 receiver 完成
	fmt.Println("程序结束")
}

ch <- value 发送数据,value := <-ch 接收数据。从已关闭的 channel 接收会返回零值和 ok = false

无缓冲 channel(同步通信)

无缓冲 channel 的发送和接收必须同时就绪,否则先执行的一方会阻塞。这实现了 goroutine 之间的同步。

package main

import (
	"fmt"
	"time"
)

func ping(ch chan string) {
	msg := <-ch // 接收
	fmt.Println("ping 收到:", msg)

	ch <- "pong" // 回复
	fmt.Println("ping 已回复")
}

func main() {
	ch := make(chan string) // 无缓冲 channel

	go ping(ch)

	// 主 goroutine 先发送
	ch <- "hello" // 如果 ping 还没准备好接收,这里会阻塞
	fmt.Println("main 已发送")

	// 等待 ping 的回复
	reply := <-ch
	fmt.Println("main 收到:", reply)
}

无缓冲 channel 保证了发送方和接收方的"握手"——双方都到达时数据才会传递。这种特性常用于 goroutine 之间的信号同步。

有缓冲 channel(异步通信)

有缓冲 channel 允许在无人接收时暂存数据,发送方只在缓冲区满时才阻塞。

package main

import "fmt"

func producer(ch chan int, count int) {
	for i := 0; i < count; i++ {
		ch <- i
		fmt.Printf("生产: %d (缓冲: %d/%d)\n", i, len(ch), cap(ch))
	}
	close(ch)
}

func main() {
	ch := make(chan int, 5) // 缓冲区容量为 5

	go producer(ch, 8)

	// 等待生产者完成一部分后开始消费
	for val := range ch {
		fmt.Printf("消费: %d (缓冲: %d/%d)\n", val, len(ch), cap(ch))
	}
	fmt.Println("所有数据处理完毕")
}

有缓冲 channel 的 len(ch) 返回当前缓冲的元素个数,cap(ch) 返回缓冲区容量。当缓冲区满时发送阻塞,当缓冲区空时接收阻塞。

channel 方向

可以限制 channel 只能发送或只能接收,增强代码的类型安全性。

package main

import "fmt"

// producer 只发送数据
func producer(ch chan<- int) {
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch)
}

// consumer 只接收数据
func consumer(ch <-chan int) {
	for val := range ch {
		fmt.Printf("收到: %d\n", val)
	}
}

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

	go producer(ch) // chan int 可以隐转换为 chan<- int
	consumer(ch)    // chan int 可以隐转换为 <-chan int

	fmt.Println("处理完毕")
}

chan<- T 表示只写 channel,<-chan T 表示只读 channel。将 chan T 传给参数时会自动转换方向限制。这种设计让函数签名清楚地表达了 channel 的使用意图。

close() 函数

close 关闭 channel,关闭后不能再发送数据,但可以继续接收。

package main

import "fmt"

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

	// 发送数据
	ch <- 1
	ch <- 2
	ch <- 3
	close(ch) // 关闭 channel

	// 关闭后仍可接收
	fmt.Println(<-ch) // 1
	fmt.Println(<-ch) // 2
	fmt.Println(<-ch) // 3

	// 缓冲区空后继续接收,返回零值
	val, ok := <-ch
	fmt.Printf("val=%d, ok=%v\n", val, ok) // val=0, ok=false

	// 关闭后发送会 panic
	// ch <- 4 // panic: send on closed channel

	// 检查 channel 是否关闭
	if !ok {
		fmt.Println("channel 已关闭且为空")
	}
}

关闭 channel 的规则:

  • 只能由发送方关闭,不能由接收方关闭
  • 关闭后不能发送,但可以接收剩余数据
  • 重复关闭会 panic
  • 向 nil channel 发送或接收会永久阻塞

range 遍历 channel

for range 可以持续从 channel 接收数据,直到 channel 被关闭。

package main

import "fmt"

func fibonacci(ch chan int, n int) {
	a, b := 0, 1
	for i := 0; i < n; i++ {
		ch <- a
		a, b = b, a+b
	}
	close(ch) // 必须关闭,否则 range 会永久阻塞
}

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

	go fibonacci(ch, 10)

	// range 会自动接收直到 channel 关闭
	fmt.Print("斐波那契数列: ")
	for val := range ch {
		fmt.Printf("%d ", val)
	}
	fmt.Println()
}

for val := range ch 等价于手动检查 ok 值的循环,代码更简洁。

nil channel 的行为

未初始化的 channel(值为 nil)在读写时都会永久阻塞。

package main

import (
	"fmt"
	"time"
)

func main() {
	var ch chan int // nil channel

	// 向 nil channel 发送会永久阻塞
	go func() {
		ch <- 1
		fmt.Println("这行不会执行")
	}()

	// 从 nil channel 接收也会永久阻塞
	go func() {
		<-ch
		fmt.Println("这行也不会执行")
	}()

	time.Sleep(100 * time.Millisecond)
	fmt.Println("主 goroutine 继续执行")

	// nil channel 的常见用途:在 select 中禁用某个 case
	readCh := make(chan int, 1)
	readCh <- 42
	var writeCh chan int // nil,暂时禁用写入

	select {
	case val := <-readCh:
		fmt.Println("读取:", val)
	case writeCh <- 1:
		fmt.Println("这行不会执行(writeCh 是 nil)")
	}
}

nil channel 在 select 语句中很有用——将某个 channel 设为 nil 可以临时禁用对应的 case 分支。

Channel 的数据流可以这样理解:发送方通过 <- 将数据放入管道,接收方通过 <- 从管道取出数据。无缓冲 channel 要求发送和接收同步就绪,有缓冲 channel 允许数据暂存。多个 goroutine 可以安全地向同一个 channel 发送或接收数据,channel 内部通过互斥锁保证并发安全。

实用模式

下面展示几个 channel 的常见使用模式。

package main

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

// 模式 1:信号通知
func signalPattern() {
	done := make(chan struct{})

	go func() {
		fmt.Println("后台任务执行中...")
		time.Sleep(100 * time.Millisecond)
		fmt.Println("后台任务完成")
		close(done) // 发送完成信号
	}()

	<-done // 等待信号
	fmt.Println("主程序继续")
}

// 模式 2:数据管道
func pipelinePattern() {
	// 生成数据
	nums := make(chan int)
	go func() {
		for i := 1; i <= 5; i++ {
			nums <- i
		}
		close(nums)
	}()

	// 平方处理
	squares := make(chan int)
	go func() {
		for n := range nums {
			squares <- n * n
		}
		close(squares)
	}()

	// 收集结果
	for s := range squares {
		fmt.Printf("%d ", s)
	}
	fmt.Println()
}

// 模式 3:扇出 - 多个 goroutine 从同一 channel 读取
func fanOutPattern() {
	jobs := make(chan int, 10)
	var wg sync.WaitGroup

	// 启动 3 个 worker
	for w := 1; w <= 3; w++ {
		wg.Add(1)
		go func(workerID int) {
			defer wg.Done()
			for job := range jobs {
				fmt.Printf("Worker %d 处理任务 %d\n", workerID, job)
				time.Sleep(50 * time.Millisecond)
			}
		}(w)
	}

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

	wg.Wait()
	fmt.Println("所有任务处理完毕")
}

func main() {
	fmt.Println("=== 信号通知模式 ===")
	signalPattern()

	fmt.Println("\n=== 管道模式 ===")
	pipelinePattern()

	fmt.Println("\n=== 扇出模式 ===")
	fanOutPattern()
}

总结

Channel 是 Go 并发通信的核心。无缓冲 channel 实现同步通信,有缓冲 channel 允许异步操作。<- 操作符用于发送和接收,close() 关闭 channel。通过 channel 方向限制(chan<-<-chan)可以增强类型安全。for range 可以简洁地遍历 channel。nil channel 在 select 中可用于禁用分支。掌握 channel 的各种模式(信号通知、管道、扇出/扇入)是编写高效并发程序的关键。