【Go】C++ 转 Go 第(五)天:Goroutine 与 Channel | Go 并发编程基础
本专栏文章持续更新,新增内容使用蓝色表示。
食用指南
本文适合有 C++ 基础的朋友,想要快速上手 Go 语言。可直接复制代码在 IDE 中查看,代码中包含详细注释和注意事项。
Go 的环境搭建请参考以下文章:
【Go】C++ 转 Go 第(一)天:环境搭建 Windows + VSCode 远程连接 Linux -CSDN博客
Go语言的并发模型建立在 Goroutine 和 channel 之上。
1. Goroutine
Goroutine 理论基础,此处留个空,后期补。
空
Goroutine 是本质上是一种由 Go 运行时(runtime)调度的轻量级线程。与传统线程相比,goroutine 的创建和销毁开销很小,其初始栈大小仅为 2KB,且支持动态扩容。程序可以同时运行多个 goroutine,它们共享相同的地址空间。
特性
-
创建机制:通过 go 关键字快速创建,支持命名函数与匿名函数两种形式
-
生命周期:主 goroutine 的退出将导致程序立即终止,所有子 goroutine 会被强制结束
-
主动控制:通过 runtime.Goexit() 可实现当前 goroutine 的立即终止
-
资源管理:结合 defer 语句确保 goroutine 退出时的资源清理与状态维护
-
通信限制:goroutine 的函数返回值无法直接获取,必须依赖 channel 机制进行跨协程数据传递
goroutine.go
package mainimport ("fmt""runtime""time"
)// 从goroutine
func newTask() {for i := 0; ; i++ {fmt.Printf("new Goroutine: i= %d \n", i)time.Sleep(1 * time.Second)}
}// 主goroutine
// 主退出,从一同退出
func main() {// 创建一个go程去执行newTask()调度go newTask()// 无参匿名函数go func() {defer fmt.Println("匿名 A defer")// 外层想退出,可以直接returnfunc() {defer fmt.Println("匿名 B defer")// 内部这层使用return只会退到外层,使用以下函数可实现退出runtime.Goexit()fmt.Println("-----B-----")time.Sleep(2 * time.Second)}() // 如果没有结尾的()相当于只定义了,未调用fmt.Println("-----A-----")time.Sleep(2 * time.Second)}()// 有参匿名函数go func(a int, b int) bool {fmt.Println("a =", a, "b =", b)return true // 从协程和主协程是一个异步的操作// 如果想要主go程得到从go程的值,需要借助管道机制}(11, 12)// 开启这部分的死循环,就不会退出了for {time.Sleep(1 * time.Second)}// fmt.Println("main Goroutine exit.\n")
}
运行结果:

2. Channel
goroutine 之间的通信通过channel(通道)实现。通道提供了一种安全、同步的方式,用于在goroutine 之间传递数据。使用通道可以避免多个 goroutine 同时访问共享数据而导致竞态条件的问题,因为管道的读写是原子性的。
2.1 无缓冲 Channel 的同步通信
无缓冲 channel 实现了 goroutine 间的同步数据交换机制,确保发送和接收操作的严格时序一致性,为并发程序提供可靠的通信基础。
1)同步通信:发送操作会阻塞,直到有接收方准备好;接收操作会阻塞,直到有数据可接收。
2)数据消费:数据具有一次性消费特性,读取后数据从 channel 中移除。
3)死锁风险:发送和接收次数必须匹配,不匹配会导致 goroutine 永久阻塞。
1_channel.go
// channel 管道,两个goroutine之间的通信机制
// 一、无缓冲的channel
package mainimport ("fmt""time"
)func main() {// 无缓冲没有地方存放数据// 1. 定义无缓冲 channelc := make(chan int)go func() {defer fmt.Println("sub goroutine exited.")fmt.Println("sub goroutine is running......")// 2. 向channel中写入c <- 666c <- 555time.Sleep(1 * time.Second)subNum := <-cfmt.Println("subNum =", subNum)}()// 思考问题:// 1) main goroutine会不会比sub goroutine先执行导致无法接收到数据,无法打印出num?// 答:无缓冲channel的同步特性确保了数据传输的时序:发送者会等待接收者,接收者会等待数据。// 2) 管道中的数据读出来之后还会有吗?// 答:channel中的数据具有一次性消费特性,读取后即从channel中移除。// 读取次数和发送次数不等会导致goroutine永久阻塞,产生deadlock// 3. 从channel中读数据// <-和c之间不能有空格<-c // 读取并丢弃num := <-c // 读取并赋值c <- 100fmt.Println("num =", num)fmt.Println("main goroutine exit.")}
运行结果:

2.2 带缓冲 Channel 的异步通信
带缓冲 channel 在同步通信基础上引入异步能力,通过固定大小的缓冲区调节生产者和消费者之间的速率差异,实现更灵活的并发控制。
异步通信:缓冲区未满时,发送操作不会阻塞;缓冲区不为空时,接收操作不会阻塞。
阻塞条件:缓冲区满时,发送操作阻塞;缓冲区空时,接收操作阻塞。
可以通过 len() 和 cap() 函数可实时获取缓冲区的当前使用情况和总容量
2_channel.go
// 二、带缓冲的channel
package mainimport ("fmt""time"
)func main() {// channel缓冲区满,写数据阻塞;channel缓冲区空,读数据阻塞// 1. 定义有缓冲 channelc := make(chan int, 2)fmt.Println("初始channel:len(c) =", len(c), "cap(c) =", cap(c))go func() {defer fmt.Println("sub goroutine exited.")fmt.Println("sub goroutine is running......")// 2. 向channel中写入for i := 0; i < 4; i++ {c <- ifmt.Println("sub向channel发送:", i, ",len(c) =", len(c), ",cap(c) =", cap(c))time.Sleep(50 * time.Millisecond)}}()// time.Sleep(1 * time.Second)for i := 0; i < 2; i++ {fmt.Println("main 接收到:", <-c, ",len(c) =", len(c), ",cap(c) =", cap(c))}time.Sleep(1 * time.Second)fmt.Println("main goroutine exited.")
}
运行结果:

2.3 Channel 的关闭与遍历
关闭操作:使用 close(c) 关闭 channel,关闭后不能再发送数据。
数据读取:已关闭的 channel 可以继续读取剩余数据,使用 data, ok := <-c 检测 channel 状态
range 遍历:自动检测 channel 关闭,简化数据读取代码。
注意:重复关闭 channel 或向已关闭 channel 发送数据都会引发运行时 panic。
3_closeChannel.go
// 使用close关闭channel
// 确定没有数据发送时,使用close关闭
package mainimport "fmt"func main() {// 定义一个channelc := make(chan int)go func() {defer fmt.Println("sub goroutine exited.")fmt.Println("sub goroutine is running......")for i := 0; i < 4; i++ {c <- i}// 关闭channelclose(c)// 如果没有close(c),会发生死锁,可以注释掉尝试一下// 如果向已关闭的channel发送数据会发生以下错误// panic: send on closed channel// c <- 99}()// for {// // 这种写法将作用域限制在if中// // 条件是ok为true// if data, ok := <-c; ok {// fmt.Println("接收到data:", data)// } else {// break// }// }// 以上代码可以优化为以下的写法for data := range c {fmt.Println("使用range接收到data:", data)}defer fmt.Println("main goroutine exited.")
}
运行结果:

2.4 Select 多路复用
select 语句允许在多个通道操作中选择一个执行。这种方式可以有效地处理多个通道的并发操作,避免了阻塞 。
多路监控:同时监听多个 channel 的读写状态,任一通道就绪即可触发相应操作。当多个 case 同时就绪时,随机选择一个执行,避免饥饿现象。
注意:所有 case 都未就绪时会阻塞,可以使用 default 实现非阻塞操作。
应用场景:超时控制(time.After创建一个定时器,在超时后执行特定的操作,避免永久阻塞。);多 channel 数据处理;goroutine 退出控制。
4_selectChannel.go
// select 可以在同一流程下监控多个channel的读写状态
// 类似C/C++中的select、poll、epoll
package mainimport "fmt"func fibonacli(c, quit chan int) {x, y := 1, 1for {select {case c <- x:// 如果 c 可写,则满足此 case// 什么时候不满足,取决于 sub 读的次数x = yy = x + ycase <-quit:fmt.Println("fibonacli end.")return}}
}func main() {c := make(chan int)quit := make(chan int)// subgo func() {for i := 0; i < 6; i++ {fmt.Println(<-c)}quit <- 0}()// mainfibonacli(c, quit) // 引用语义
}
运行结果:

如有问题或建议,欢迎在评论区中留言~
