Go channel

channel 在 goroutine 之间传递值并建立同步点:缓冲与无缓冲、关闭与排空、nil 阻塞、所有权与 happens-before。

#type / concept #status / growing #tech / dev #resource / go

[!info] 关联笔记

Go channel

这个概念为什么会出现

多个 goroutine 若只靠共享内存 + 锁,正确性与可读性成本都高。Go 把进程内的通信做成类型系统的一部分:

ch := make(chan int)
ch <- 1
v := <-ch

channel 同时做两件事:传值同步。它不是 Kafka 式分布式队列,而是语言级并发原语。

[!abstract] 一句话理解 channel 是类型化的通信管道;发送与接收在规则满足时交接数据,并在 Go 内存模型下建立 happens-before 关系。关闭表示“不会再有新值”,通常由发送方负责。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:健康检查探针把结果交回主流程

服务启动时要跑一次依赖探测(ping Redis/DB)。
探测在后台 goroutine 做,主流程必须拿到结果字符串后才能决定是否对外服务

用无缓冲 channel:

  1. 传值:把 "pong" / 错误信息递回来
  2. 同步:发送与接收握手——主流程打印/判断一定发生在探测方写入之后

这比“共享变量 + 睡一会再读”可靠得多。

package main

import "fmt"

// probeDependency 模拟“后台探测依赖是否存活”。
// 业务意图:慢 I/O 放在 goroutine;结果只通过 out 交回,不写共享变量。
// 教学点:ch <- v 在无缓冲 channel 上会等到有人接收。
func probeDependency(out chan<- string) {
	// 真实项目里:ping Redis、HEAD 健康 URL 等。
	// 成功就送 "pong";失败可送 "down: ..." 或改用 struct{ok,err}。
	out <- "pong"
}

func main() {
	// 无缓冲 channel:发送与接收必须配对,本身就是同步点。
	result := make(chan string)

	// 启动探针;main 继续往下走到接收处等待。
	go probeDependency(result)

	// 阻塞直到探针写入;读到的值一定 happens-after 发送侧赋值完成。
	status := <-result
	fmt.Println("dependency:", status) // dependency: pong
}

建议运行:

go run .

期望输出:

dependency: pong

结合场景再看三个关注点

  1. channel = 传值 + 同步
    主流程不是“猜探针写完没有”,而是在 <-result 上明确等待。

  2. 无缓冲要求双方握手
    适合“结果必须被看见才继续”的启动探针、一次性信号。

  3. 所有权清晰:谁发送、谁接收
    本例只有探针发送、main 接收;关闭与多发送方规则见后文。

核心概念与准确模型

创建

make(chan T)      // 无缓冲
make(chan T, n)   // 缓冲容量 n
var ch chan T     // nil channel

单向类型用于 API 边界:

func produce(out chan<- int) {}
func consume(in <-chan int) {}

无缓冲 vs 有缓冲

无缓冲有缓冲
发送需接收方准备好缓冲未满即可返回
接收需发送方准备好缓冲非空即可返回
典型用途同步交接、信号平滑速率、有限队列

缓冲容量是本地资源上限,不是无限队列。

关闭与排空

close(ch)
v, ok := <-ch // ok=false 表示已关闭且无更多值
for v := range ch {
    // 直到关闭并排空
}

规则:

  1. 只有发送方(所有权方)应关闭
  2. 向已关闭 channel 发送 → panic
  3. 重复关闭 → panic
  4. 接收已关闭:先读完剩余缓冲,再得到零值且 ok=false
  5. 关闭不是“释放所有资源”的通用析构,而是“流结束”信号

状态表

状态接收发送close
nil永久阻塞永久阻塞panic
打开,可进行得值成功成功
已关闭,仍有缓冲得缓冲值panicpanic
已关闭,已排空零值,ok=falsepanicpanic

与内存模型

channel 通信建立同步:发送 happens-before 对应接收完成。详见 Go Memory Model — Channel communicationgo-happens-before-and-synchronization

nil channel 与 select

ch = nil 可禁用 select 的某个 case(永久不选中)。这是取消某路通信的惯用技巧。

设计动机

口号式表述常写作 Do not communicate by sharing memory; share memory by communicating。更精确地说:

  • 先用数据流表达并发结构
  • 锁仍适用于共享状态结构
  • channel 让“谁在何时可见什么”更局部

边界情况

  1. 死锁:无缓冲互相等待、main 只发送不接收等,可能被运行时检测并 fail。
  2. 泄漏:发送方无人接、接收方无人发。
  3. 关闭权争夺:多发送方时需额外协调(如 WaitGroup 后再 close)。
  4. 传递指针:channel 传的是值;指针指向的数据仍共享,需约定所有权。
  5. 不是广播:默认一对一交接;广播需多 channel 或其他结构。

常见误区

[!warning] 常见误区:接收方 close 错误:消费者关闭共享 channel。
正确:发送所有权方关闭;或文档明确单一 closer。

[!warning] 常见误区:用 channel 替代所有锁 错误:复杂共享图硬套 channel 更乱。
正确:结构所有权清晰时用 channel;共享 map/计数器用 mutex/原子。

[!warning] 常见误区:无限缓冲幻想 错误:make(chan T, 1<<30) 当消息队列。
正确:有界 + 背压 + 超时/取消。

工程实践

  1. 所有权写进函数签名(单向 channel)
  2. 关闭即结束流,与 range 搭配
  3. select + 超时/ctx 防永久阻塞(go-select
  4. 错误与值一起传struct { Val T; Err error } 或双 channel 并文档化
  5. 测试并发-race 与确定性同步,避免 time.Sleep 当正确性条件

可验证实验

  1. 无缓冲发送无接收 → 死锁。
  2. closerange 排空。
  3. 向关闭 channel 发送 → panic。
  4. nil channel 在 select 中被禁用。
  5. 缓冲为 1 时连续发送第二次是否阻塞。

本节总结

  • 本质:类型化进程内管道 + 同步点。
  • 关键规则:关闭所有权、状态表、nil 阻塞、内存模型同步。
  • 最易错:错关、泄漏、当分布式队列。
  • 下一步select 与取消组合。

自测题

概念题

  1. 无缓冲 channel 的同步含义是什么?
  2. 谁应该 close?
  3. 已关闭 channel 上接收的 ok 何时为 false?

代码推理题

ch := make(chan int, 1)
ch <- 1
close(ch)
v1, ok1 := <-ch
v2, ok2 := <-ch
fmt.Println(v1, ok1, v2, ok2)

工程思考题

多个 worker 写入同一结果 channel,如何安全关闭?

参考答案

展开
  1. 发送与接收完成形成交接与同步。

  2. 发送所有权方。

  3. 关闭且缓冲排空后。
    代码:1 true 0 false
    工程:WaitGroup 等所有发送者结束后由协调者 close;或每个 worker 独立 channel 再 fan-in。

延伸阅读与资料来源

资料类型支撑
Spec — Channel types规范类型与操作
Spec — Close规范close 语义
Memory Model — Channel规范同步
Effective Go — Channels文档用法风格
Share Memory By Communicating博客设计示例

笔记元信息

  • 建议文件名:go-channels.md
  • 所属阶段:阶段四
  • 本篇状态:已深化
  • 建议下一篇:Go select
创建于 2026/6/20 更新于 2026/7/15