channel
为什么需要channel
使用全局变量加锁同步来解决goroutine的竞争,可以但不完美
-
难以精确控制等待时间(主线程无法准确知道所有 goroutine 何时完成)。
-
全局变量容易引发竞态条件(即使加锁,代码复杂度也会增加)。
-
不够优雅,Go 更推荐使用 channel 进行通信。
channel基本介绍
- channel(通道) 是一种用于 goroutine(协程)之间通信和同步 的机制。
- channel的本质是一个队列,遵循先进先出(FIFO)
- channel是有类型的,一种channel只能存储类型与该channel的类型相同的数据
- channel是线程安全的,不需要加锁
- channel是引用类型,必须初始化make后才能写入数据,未初始化的channel是nil
channel快速入门
channel的声明
var 变量名 chan 数据类型
channel的初始化
var myChan chan int = make(chan int, 2)
发送数据到channel
myChan <- 10
myChan <- 20
此处注意:channel不像map等会自动扩容,channel接收的数据数的最大值在make函数里已经自定义完成了,容量就是一直这么大,不会改变。
从channel接收数据
num := <-myChan
fmt.Println(num)
输出结果:10
此处接收数据不能超出myChan现有的数据量,也就是myChan的长度。
channel的细节
-
channel中只能存放指定的数据类型
-
channle的数据放满后,就不能再放入了
-
channel放满后,如果从channel取出数据后,可以继续放入
<-myChan也是取出了数据,只是没有被接收罢了
- 在没有使用协程的情况下,如果channel数据取完了,再取,就会报deadlock
易错点
如果想在channel中输入多样的数据类型,就将channel声明成空接口interface{}的类型
代码示例:
func main() {
myChan := make(chan interface{}, 3)
myChan <- 10
myChan <- “sa”
person := Person{“xxx”}
myChan <- person
<-myChan
<-myChan
person2 := <-myChan
// fmt.Printf(“person2的type%T,值%v,name%v”, person2, person2, person2.Name)
person3 := person2.(Person)
fmt.Printf(“person3的type:%T,值:%v,name:%v”, person3, person3, person3.Name)
}
输出结果:person3的typemain.Person,值{xxx},namexxx
唉,为什么要person3 := person2.(Person)呢,直接用fmt.Printf(“person2的type%T,值%v,name%v”, person2, person2, person2.Name)不好吗?
当然不好啦,使用这个代码会报错,会说person2.Name undefined
为什么呢?
因为person2是从channel中读取的interface{}类型,虽然实际值是Person类型,但编译器不知道其具体类型,因此无法直接访问Name字段。
所以要通过person3 := person2.(Person),提取一个类型断言后的值,将其转换为具体的Person类型,然后才能访问其字段
channel的关闭
发送方可以关闭 channel,表示不再发送数据
内置函数:close(ch)
•关闭后,仍然可以接收数据(直到 channel 为空)。
•向已关闭的 channel 发送数据会 panic。
channel的遍历
- 通过 for-range 遍历
代码示例:
func main() {
myChan := make(chan int, 3)
myChan <- 10
myChan <- 30
myChan <- 20
close(myChan)
for v := range myChan {
fmt.Printf(“%v\n”, v)
}
}
输出结果:
10
30
20
for range会一直从 ch接收数据,直到 ch被关闭。
如果 ch未关闭,for range会一直阻塞,可能导致死锁。
手动检查 channel 是否关闭
可以用 value, ok := <-ch的方式检查 channel 是否关闭, 如果 channel 关闭,ok 为 false
- 传统for循环
func main() {
myChan := make(chan int, 3)
myChan <- 10
myChan <- 30
myChan <- 20
len := len(myChan)
for i := 0; i < len; i++ {
fmt.Println(<-myChan)
}
}
也可以正常有序输出,输出结果与for-range一致
channel的阻塞
阻塞是指 goroutine 在 channel 操作上等待,但不会导致整个程序卡死。
- 从空的 channel 接收数据
- 向已满的缓冲 channel 发送数据
- 读比写的操作慢,导致出现(2)情况
- 写比读的操作慢,导致出现(1)情况
channel的死锁
死锁是指所有 goroutine 都在等待对方释放资源,导致程序无法继续执行。
- 所有 goroutine 都在等待 channel
- 未关闭 channel 导致 for range死锁
使用细节
- channel可以声明为只读,或者只写性质
此处只读只写只是一种属性,并不会改变channel的类型,该是chan int 就还是chan int
chan<- int 是只写
<-chan int 是只读
代码示例:
package main
import (
“fmt”
“math/rand”
“time”
)
// 只写通道:用于发送订单
func orderProducer(orderChan chan<- int, doneChan chan<- struct{}) {
defer close(orderChan) // 生产结束后关闭订单通道
for i := 1; i <= 5; i++ {orderID := rand.Intn(1000) + 1000 // 模拟生成订单号fmt.Printf("📦 生成订单 #%d (ID: %d)\n", i, orderID)orderChan <- orderIDtime.Sleep(time.Second) // 模拟生产间隔
}doneChan <- struct{}{} // 发送完成信号
}
// 只读通道:用于处理订单
func orderProcessor(orderChan <-chan int, doneChan chan<- struct{}) {
for orderID := range orderChan { // 自动检测通道关闭
processTime := time.Duration(rand.Intn(1500)) * time.Millisecond
fmt.Printf(“处理订单 ID: %d (耗时: %v)\n”, orderID, processTime)
time.Sleep(processTime)
}
doneChan <- struct{}{} // 发送完成信号
}
func main() {
// 初始化通道(带缓冲)
orderChan := make(chan int, 3) // 订单通道(缓冲3个订单)
doneChan := make(chan struct{}, 2) // 控制通道(缓冲2个信号)
// 启动服务
go orderProducer(orderChan, doneChan) // 订单生产(只写)
go orderProcessor(orderChan, doneChan) // 订单处理(只读)// 等待两个服务完成
for i := 0; i < 2; i++ {<-doneChan
}
fmt.Println("所有订单处理完成")
}
这段代码中,main函数中定义的orderChan是一个chan int 类型,但他可以同时被使用在只读和只写的函数里,这就很大程度上的便于代码的管理,防止误操作。
- select解决 channel 阻塞问题
日常中,难以准确判断读取/写入与关闭时机难以掌握,所以提出select,虽然select还是无法关闭channel,但是能防止防止读取/写入时的无限等待
代码示例
for{
select {
case msg := <-ch1:
fmt.Println(“收到 ch1:”, msg)
case msg := <-ch2:
fmt.Println(“收到 ch2:”, msg)
case <-time.After(3 * time.Second): // 超时控制
fmt.Println(“读取超时”)
return
}
}
如果多个 case 的 channel 同时就绪(例如多个 channel 都有数据可读),select会随机选择一个执行(公平调度,避免饥饿问题)
select自动忽略未就绪的 channel(无论是否关闭),无需手动处理。
此处的ch1哪怕没有关闭,也不会报错,而是在无法从ch1取到值后,会暂时将这个case不考虑在执行case内
每次执行 select时都会重新检查所有 case的就绪状态
还有,最后的return不能使用break代替,因为break只能退出select不能退出for循环,所以相当于重新开始了
return其实还可以用之前提到的label来代替,就是给这个for循环一个标签,然后break label就好了 (但是这种方式并不建议,可读性较差)
- recover来防止出现因为一个线程的错误导致其它线程无法进行
原错误代码:
package main
import (
“fmt”
“time”
)
// 1. 循环打印 “hello,world”
func sayHello() {
for i := 0; i < 10; i++ {
fmt.Println(“hello,world”)
time.Sleep(1 * time.Second)
}
}
// 2. 测试未初始化的 map(会触发 panic)
func test() {
var myMap map[int]string
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
myMap[0] = “golang” // error: 未初始化的 map 赋值会导致 panic
}
// 3. 主函数(并发执行)
func main() {
go sayHello() // 启动协程
go test() // 启动协程(会崩溃)
// 主线程继续执行
for i := 0; i < 10; i++ {fmt.Printf("main() ok=%d\n", i)time.Sleep(1 * time.Second)
}
}
输出结果:
main() ok=0
hello,world
panic: assignment to entry in nil map
报了panic错误,主线程并没有正常运行
修改代码:
func test() {
var myMap map[int]string
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
myMap[0] = “golang” // error: 未初始化的 map 赋值会导致 panic
}
将错误的test函数加上错误捕获,异常处理
输出结果:
hello,world
assignment to entry in nil map
main() ok=0
main() ok=1
hello,world
hello,world
main() ok=2
hello,world
main() ok=3
hello,world
main() ok=4
hello,world
main() ok=5
main() ok=6
hello,world
hello,world
main() ok=7
main() ok=8
hello,world
hello,world
main() ok=9
即使仍是错误,也依旧不影响其它线程