Go语言并发实现完整代码示例
作者:星星懒得动
前言
并发/并行
并发:多线程程序在单核心的 cpu 上运行,称为并发;
并行:多线程程序在多核心的 cpu 上运行,称为并行。
并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。
简单比喻:
并发像一个人同时煮饭、洗衣服和打电话,通过任务切换来完成多项工作
并行像三个人分别煮饭、洗衣服和打电话,真正同时进行
go语言:只需要简单几行,可以轻松写出一堆并发函数
实现
go routines
下面是几个小例子,方便理解异步
示例一
package main
import "fmt"
func someFunc(name string){
fmt.Println(name)
}
func main(){
someFunc("Lucy")
fmt.Println("hello")
}运行结果:
![]()
在main函数中调用someFunc函数,主函数会被卡住,直到这个函数执行完毕才会继续,所以会先打印Lucy,然后打印hello
示例二
使用go routine可以使主函数不用等这个函数执行就能结束,主线程可以将任务分叉出去
package main
import "fmt"
func someFunc(name string){
fmt.Println(name)
}
func main(){
go someFunc("Lucy")
go someFunc("Lili")
fmt.Println("hello")
}运行结果
![]()
然后会发现上面的结果都没有打印,只打印hello。这是因为主进程开了一些子进程,子进程会自己跑起来,执行任务,而主进程把任务派发出去立马会忙自己的事。
示例三
可以给他加个延deng
package main
import "fmt"
import "time"
func someFunc(name string){
fmt.Println(name)
}
func main(){
go someFunc("Lucy")
go someFunc("Lili")
fmt.Println("hello")
time.Sleep(time.Second * 2)
}运行结果

可以看到someFunc的打印,但是多运行几次,可以看到,结果是随机的,因为他们都是异步执行的
小结:go 语言采用的是fork join并发模型,下图能展示他的工作原理,展示了主函数的生命周期
fork 指的是在程序中的任意地方可以将执行的子分支与父节点同时运行
join指的是,在未来的某时刻,这些并发的分支可以合并在一起

channels
简单来说,channel 就像是 goroutine 之间的管道或消息队列,让不同的并发执行单元能够安全地交换数据。
channel的主要用途:
goroutine之间的通信:channel允许一个goroutine向另一个goroutine发送数据。
同步:channel可以用于控制goroutine的执行顺序,例如等待一个goroutine完成。
创建channel
使用make函数创建,可以指定channel中传递的数据类型以及可选的缓冲区大小。
ch := make(chan int) // 无缓冲channel chBuffered := make(chan int, 10) // 有缓冲channel,缓冲区大小为10
发送数据:
使用<-操作符,将数据发送到channel。
ch <- 42 // 将42发送到channel ch
接收数据:
使用<-操作符,从channel中接收数据。
value := <-ch // 从channel ch接收数据并赋值给value
关闭channel:
使用close函数。关闭后,不能再向channel发送数据,但可以继续接收直到数据被取完。
close(ch)
Channel 关闭的本质变化
关闭 Channel 的效果:
| 操作 | 关闭前 | 关闭后 |
|---|---|---|
发送数据 ch <- data | 正常(缓冲区未满时) | panic |
接收数据 <-ch | 阻塞(无数据时) | 立即返回零值 + ok=false |
再次关闭 close(ch) | - | panic |
range 遍历 for v := range ch | 正常遍历 | 自动退出循环 |
关闭 Channel 后的接收行为总结
| Channel 状态 | 接收操作 value, ok := <-ch | 说明 |
|---|---|---|
| 关闭且有数据 | value=数据, ok=true | 能正常取出剩余数据 |
| 关闭且无数据 | value=零值, ok=false | 立即返回零值 |
| 未关闭且有数据 | value=数据, ok=true | 正常接收 |
| 未关闭且无数据 | 阻塞等待 | 直到有数据或 channel 关闭 |
总结
关闭有数据的 channel:可以继续取出所有剩余数据,取完后返回
(零值, false)关闭无数据的 channel:立即返回
(零值, false)关键区别:
ok标志告诉你是否真的收到了数据,还是只是因为 channel 关闭而返回的零值
这种设计让 Go 的 channel 能够优雅地实现"数据流结束"的信号传递!
最佳实践
遵循"谁创建,谁关闭"的原则
使用 defer 和 recover 保护可能 panic 的代码
考虑使用 context 进行优雅的 goroutine 生命周期管理
在关闭 channel 前确保没有 goroutine 会再向它发送数据
记住:向已关闭的 channel 发送数据是 Go 中的经典错误,一定要避免!
通道分类
无缓冲channel与有缓冲channel:
无缓冲channel: 发送和接收操作是同步的。发送操作会阻塞,直到另一个goroutine在对应的channel上执行接收操作,反之亦然。这种特性使得无缓冲channel非常适合用于两个goroutine之间的同步。
同步通信:发送和接收必须同时准备好
发送操作会阻塞,直到有 goroutine 接收
接收操作会阻塞,直到有 goroutine 发送
有缓冲channel:发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区空时才会阻塞。有缓冲channel提供了一种异步通信的方式,但要注意缓冲区大小,如果缓冲区满了,发送操作会阻塞。
异步通信:缓冲区未满时可立即发送,未空时可立即接收
只有当缓冲区满时发送才会阻塞
只有当缓冲区空时接收才会阻塞
特性
| 无缓冲 Channel | 有缓冲 Channel | |
|---|---|---|
| 同步性 | 强同步 | 弱同步 |
| 阻塞时机 | 发送/接收必须配对 | 缓冲区满/空时才阻塞 |
| 使用场景 | 精确的goroutine同步 | 生产消费解耦、流量控制 |
| 类比 | 电话通话 | 邮件信箱 |
select
在Go语言中,select语句用于在多个通道操作(发送或接收)之间进行选择。它会一直等待,直到其中一个通道操作可以执行,然后执行该操作。如果多个通道操作都可以执行,它会随机选择一个执行。
select语句是Go语言中处理并发编程的重要工具,它允许goroutine同时等待多个通道操作,从而可以实现多路复用。
基本语法
select {
case sendOrReceive1:
// 执行相应的操作
case sendOrReceive2:
// 执行相应的操作
default:
// 如果以上都没有准备就绪,则执行default
}特点
随机选择:如果多个case同时就绪,
select会随机选择一个执行。阻塞:如果没有case就绪,且没有default分支,
select会一直阻塞,直到某个case就绪。非阻塞:如果有default分支,当没有case就绪时,会执行default分支,从而避免阻塞。
使用场景
多通道操作:当需要同时从多个通道接收数据,或者向多个通道发送数据时。
超时控制:通过
time.After通道实现超时控制。非阻塞操作:使用default分支实现非阻塞的发送或接收。
package main
import "fmt"
func main(){
mychannel := make(chan string)
anotherChannel := make(chan string)
go func() {
mychannel <- "data" //发送数据到channel
}()
go func() {
anotherChannel <- "data 2" //发送数据到channel
}()
// 主goroutine 从channel接收数据
select {
case msgFromMychannel := <- mychannel:
fmt.Println(msgFromMychannel)
case msgFromAnotherChannel := <- anotherChannel:
fmt.Println(msgFromAnotherChannel)
}
}
超时控制
func main() {
ch := make(chan string)
go func() {
time.Sleep(3 * time.Second) // 模拟耗时操作
ch <- "操作完成"
}()
select {
case result := <-ch:
fmt.Println("成功:", result)
case <-time.After(2 * time.Second): // 2秒超时
fmt.Println("操作超时!")
}
}运行结果
![]()
for-select loop
示例一
package main
import "fmt"
func main(){
charchannel := make(chan string, 3) //带缓冲
chars := []string{"a","b","c"}
for _,s := range chars{
select{
case charchannel <-s:
}
}
close(charchannel) //关闭通道之后也能读
for result := range charchannel {
fmt.Println(result)
}
}运行结果

示例二:无限循环协程
package main
import "fmt"
import "time"
func someFunc(){
for{
select{
default:
fmt.Println("ongoing")
}
}
}
func main() {
go someFunc()
time.Sleep(time.Second * 10)
}运行结果:无线循环打印,但是因为主goroutine 10s退出了,所以终止了:

done channel
示例三
在someFunc中,有一个无限循环,循环中使用select监听两个case:一个是从done中接收数据,另一个是default。
select语句会非阻塞地检查各个case。如果done通道有数据可读(或者被关闭),那么case <- done这个分支就会执行,然后return退出循环
package main
import "fmt"
import "time"
func someFunc(done <- chan bool){ //将done传递给someFunc
for{
select{//非阻塞检查各个case
case <- done:
return
default:
fmt.Println("ongoing")
}
}
}
func main() {
done := make(chan bool)
go someFunc(done)
time.Sleep(time.Second * 3)
close(done) //关闭done通道
}示例四
package main
import "fmt"
import "time"
func doWork(done chan bool) {
fmt.Println("Working...")
time.Sleep(2 * time.Second)
fmt.Println("Done!")
done <- true // 发送完成信号
}
func main() {
done := make(chan bool)
go doWork(done)
<-done // 等待完成信号(阻塞直到收到数据)
fmt.Println("Work completed!")
}运行结果: 打印Working之后sleep 2s 打印Done

pipelines
Pipeline(管道) 是 Go 并发编程中的一种重要模式,它通过连接多个阶段的 channel 操作来处理数据流,每个阶段都是一组执行相同函数的 goroutine。
Pipeline 的核心概念
Pipeline 就像工厂的流水线,数据像零件一样在不同工位(阶段)间传递和处理
数据源 → 阶段1 → 阶段2 → ... → 阶段N → 结果
Go Pipeline 的核心价值:
数据流处理:将复杂任务分解为简单的阶段
并发控制:自动处理 goroutine 间的同步
资源管理:通过 channel 控制内存和 CPU 使用
错误处理:统一的错误传播机制
可组合性:像乐高积木一样组合不同阶段
Pipeline 模式让并发编程变得:
✅ 更清晰:每个阶段职责单一
✅ 更安全:自动处理并发问题
✅ 更灵活:易于扩展和修改
✅ 更高效:充分利用多核性能
总结
到此这篇关于Go语言并发实现的文章就介绍到这了,更多相关Go语言并发实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
