你会彻底掌握:
make(chan T)和make(chan T, 0)、make(chan T, n)到底有什么区别- 通道创建后底层长什么样
- 容量 = 0 / 容量 > 0 行为差异
- nil 通道 vs 已 make 通道
- 如何根据业务选择正确的通道容量
- 面试 100% 问的核心知识点
一、通道为什么必须用 make?
Channel 是 引用类型,和 slice、map 一样。
- 只声明
var ch chan int,得到的是 nil 通道 - nil 通道不能用,发送/接收会永久阻塞
- 必须用
make分配内存、初始化底层结构体,通道才能工作
go
var ch chan int // nil 通道,不能用
ch = make(chan int) // 初始化后,才能用
二、make 创建通道的 3 种标准写法
go
// 1. 无缓冲通道(容量 0)
ch := make(chan T)
// 2. 无缓冲通道(等价写法)
ch := make(chan T, 0)
// 3. 有缓冲通道(容量 N)
ch := make(chan T, N)
核心参数解释
- T:通道传输的数据类型(int/string/struct/interface 等)
- 容量 :
- 0 = 无缓冲(同步)
- N > 0 = 有缓冲(异步)
三、make(chan T) 无缓冲通道(容量=0)
行为规则
- 必须发送和接收同时就绪
- 发送方会阻塞,直到有人接收
- 接收方会阻塞,直到有人发送
- 强同步
示例
go
ch := make(chan int) // 无缓冲
go func() {
ch <- 100 // 阻塞,直到 main 取走
}()
fmt.Println(<-ch) // 取走,发送方继续
底层特点
- 不创建缓冲区数组
- 数据直接从发送goroutine拷贝到接收goroutine
- 纯粹用于同步、信号通知
四、make(chan T, N) 有缓冲通道(N>0)
行为规则
- 缓冲区未满:发送不阻塞
- 缓冲区为空:接收不阻塞
- 缓冲区满:发送阻塞
- 缓冲区空:接收阻塞
- 异步通信
示例
go
ch := make(chan int, 2) // 容量2
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
ch <- 3 // 阻塞(缓冲区满)
底层特点
- 创建一个长度为 N 的环形数组作为缓冲区
- 发送先写缓冲区,接收先读缓冲区
- 解耦发送和接收,削峰、限流、队列
五、make 创建通道的底层原理(极简版)
执行 make(chan T, size) 时,Go 运行时会:
- 分配
hchan结构体内存 - 根据容量决定是否创建环形缓冲区数组
- 初始化锁、等待队列、下标指针
- 返回通道引用(指针)
核心底层结构
go
type hchan struct {
qcount uint // 当前元素数量
dataqsiz uint // 缓冲区容量(make 传入的 N)
buf unsafe.Pointer // 环形缓冲区
elemsize uint16 // 元素大小
closed uint32 // 是否关闭
sendx uint // 发送下标
recvx uint // 接收下标
recvq waitq // 等待的接收者
sendq waitq // 等待的发送者
lock mutex // 并发安全锁
}
一句话总结 :
make 就是给通道分配内存 + 初始化缓冲区 + 准备运行环境。
六、nil 通道 vs make 初始化通道(超级重要)
| 操作 | nil 通道 var ch chan int |
已 make 通道 ch := make(chan int) |
|---|---|---|
| 发送 | 永久阻塞 | 正常/阻塞 |
| 接收 | 永久阻塞 | 正常/阻塞 |
| close | panic | 正常关闭 |
| len(ch) | 0 | 缓冲区元素个数 |
| cap(ch) | 0 | 缓冲区容量 |
| select case | 永远不就绪 | 正常就绪 |
结论 :
不 make 的通道完全不可用,只会造成 goroutine 泄漏、死锁。
七、len() 和 cap() 对通道的意义
go
ch := make(chan int, 3)
ch <- 1
ch <- 2
fmt.Println(len(ch)) // 2(缓冲区里有多少数据)
fmt.Println(cap(ch)) // 3(缓冲区总容量)
- len(ch) :通道缓冲区中待读取的元素数量
- cap(ch) :创建时
make指定的总容量
无缓冲通道:
go
len(ch) = 0
cap(ch) = 0
八、make 创建单向通道
你不能直接 make 一个只能在当前上下文使用的单向通道,但可以约束:
go
// 只写
var sendCh chan<- int = make(chan int)
// 只读
var recvCh <-chan int = make(chan int)
作用:用于函数参数,限制权限,提高代码安全。
go
func send(ch chan<- int) { // 只写
ch <- 100
}
func recv(ch <-chan int) { // 只读
<-ch
}
九、通道容量到底该怎么选?(实战指南)
1. 容量 = 0(无缓冲)
适用:
- 同步等待
- 信号通知
- 必须严格等待对方响应
2. 容量 = 1
适用:
- 互斥锁(类似 mutex)
- 简单串行控制
3. 容量 > 1(有缓冲)
适用:
- 流量控制
- 任务队列
- 生产者消费者解耦
- 削峰填谷
4. 容量如何确定?
- 能小不要大
- 不要盲目设很大(如 10000),会掩盖并发问题
- 限流场景:容量 = 最大并发数
十、make 通道常见坑(90% 的人中招)
坑1:只声明不 make
go
var ch chan int
ch <- 1 // 永久阻塞 → goroutine 泄漏
坑2:无缓冲通道在同一个 goroutine 收发
go
ch := make(chan int)
ch <- 1 // 阻塞,没人接收 → 死锁
坑3:把容量当成"最多发送次数"
容量是缓冲区大小 ,不是发送上限。
满了就阻塞,不是报错。
坑4:make(chan T, -1)
编译错误,容量不能为负数。
坑5:以为 len(ch) == 0 就是通道关闭
错误!
关闭判断必须用:
go
v, ok := <-ch
if !ok {
// 已关闭
}
十一、面试必问(你看完就能秒答)
Q1:make(chan int) 和 make(chan int, 0) 一样吗?
完全一样,都是无缓冲通道。
Q2:通道容量为 0 和 1 有什么区别?
- 0:同步,必须收发同时就绪
- 1:异步,可以缓存1个值,发送1次不阻塞
Q3:nil 通道可以用吗?
不可以,收发都会永久阻塞。
Q4:无缓冲通道底层有缓冲区吗?
没有,数据直接 goroutine 到 goroutine。
Q5:cap(ch) 返回什么?
make 时指定的容量。
Q6:为什么通道必须 make?
通道是引用类型,需要分配底层 hchan 结构体和缓冲区。
十二、终极总结(一句话记住)
make(chan T) 创建无缓冲同步通道;
make(chan T, N) 创建有缓冲异步通道;
不 make 就是 nil 通道,永远阻塞;
容量决定是否异步、缓冲区大小。