当前位置: 首页 > ai >正文

GO协程(Goroutine)问题总结(待续)

在使用Go语言来编写代码时,遇到的一些问题总结一下
[参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html

1. main()函数默认的Goroutine

场景再现:今天在看到这个教程的时候,在自己的电脑上跑了一下示例的代码。
发现了描述与我的执行结果不同的地方,官方文档描述如下:

这一次的执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!。

但是我执行后的情况是,如图:
 main()函数默认的Goroutine

可以看到,我最终的执行结果是都输出了,而不是只输出了
main goroutine done!
Why?
原因是——虽然 main() 函数中调用了 go hello(),主 goroutine 在打印完 main goroutine done! 后就会退出,但:

在主 goroutine 退出前,如果新启动的 goroutine 有足够的时间运行完,Hello Goroutine! 就会输出。
上面这段代码启动了一个新 goroutine,但程序的执行是并发的,不是同步/阻塞的。

执行流程是:

go hello() 启动了一个新 goroutine;

fmt.Println(“main goroutine done!”) 被执行;

如果此时 main() 返回前,新 goroutine 还没来得及执行完,那它也会被强行终止;

但如果它已经执行完了,就能看到打印的内容。

这两句都成功输出,是因为你的电脑配置比较好,执行速度非常快,新启动的 goroutine 来得及在 main() 退出前完成打印。

正确做法:用 sync.WaitGroup 或 time.Sleep

2 . Go 协程(Goroutine)的两个关键点

协程不能保证执行的顺序,但是如果加了time.sleep的话,可以保障协程执行完毕

✅ Go 协程(Goroutine)的两个关键点:
①. 协程是并发的,不能保证执行顺序
go hello() 启动后,什么时候运行是由 Go 调度器决定的。

主协程和子协程是“谁抢到 CPU 谁先跑”,谁先打印是不确定的。

所以:

go hello()
fmt.Println("main done")

有可能先打印 main done,也可能先打印 Hello,取决于当时调度情况。

② 加 time.Sleep() 可以“间接保障”子协程执行完
加 time.Sleep() 相当于强行让主协程等一下,给子协程留时间执行完。

所以子协程通常会有时间执行完,看起来“像是被保障了执行”。
❗但注意:time.Sleep() ≠ 可靠同步
虽然 time.Sleep() 很简单,但它存在几个问题:

问题点说明
❌ 不精准你不知道子协程到底需要多少时间,sleep 多了浪费,少了又执行不完
❌ 不可扩展如果你有多个协程,就很难 sleep 到合适的时间
✅ 适合临时调试用于演示或实验是可以的

✅ 正确做法:用 sync.WaitGroup


import ("fmt""sync"
)func hello(wg *sync.WaitGroup) {fmt.Println("Hello Goroutine!")wg.Done() // 协程结束,通知 WaitGroup
}func main() {var wg sync.WaitGroupwg.Add(1)        // 告诉 WaitGroup 等待 1 个协程go hello(&wg)    // 启动协程wg.Wait()        // 等待所有协程结束fmt.Println("main goroutine done!")
}

这样就能准确地等待协程执行完再退出,不用靠 sleep。

总结一句话:
time.Sleep() 是简单粗暴的等待方式,可以在小程序中“凑合用”,但真正写程序,用 sync.WaitGroup 等同步机制更稳、更准、更专业。

3.defer

3.1 defer使用对比
func hello(i int) {defer wg.Done() // goroutine结束就登记-1fmt.Println("Hello Goroutine!", i)
}func hello(i int){fmt.Println("Hello Goroutine!",i)wg.Done()
}
//在 正常情况下的效果是一样的:都会确保在 goroutine 执行完成后调用 wg.Done(),
//从而通知 WaitGroup,减少一个等待计数。

✅ defer wg.Done() 的优势:
defer 会在 函数返回前自动执行,即使函数中间发生了 panic(未被恢复),defer 也会运行(前提是没有让程序直接崩溃)。

这意味着:

func hello(i int) {defer wg.Done()// 如果这里出现错误,也能保证 Done 会执行fmt.Println("Hello Goroutine!", i)
}

更安全、稳健,防止遗漏。

❗ 手动调用 wg.Done() 有风险:

func hello(i int) {fmt.Println("Hello Goroutine!", i)wg.Done()
}

如果你写了更复杂的逻辑,中途 return 或 panic 了,wg.Done() 可能根本执行不到,就会导致 wg.Wait() 永远卡住。
✅ 总结:

写法是否推荐原因
defer wg.Done()✅ 推荐更安全,即使中途出错也能保证 Done 被调用
手动调用 wg.Done()⚠️ 慎用必须确保函数末尾一定能执行到,否则容易漏调用

所以你的判断是对的:“两种写法是一样的”,功能上是对的;但为了避免未来的问题,推荐使用 defer 写法,更健壮、易维护。

3.2defer的作用、在Goroutine中的使用案例以及执行顺序
3.2.1 defer的作用

✅ defer 的作用
defer 用于延迟一个函数的执行,直到外围函数(即当前函数)返回之前才调用。

换句话说:

无论当前函数中发生了什么(正常结束或提前 return),defer 注册的语句都会在函数结束前自动执行。

📌 举个例子说明:

func demo() {fmt.Println("start")defer fmt.Println("this is defer")fmt.Println("end")
}

输出结果是:

start
end
this is defer

defer fmt.Println(“this is defer”) 被延迟执行到 demo() 函数退出前的最后一刻。

✅ defer 的常见用途

用途示例说明
释放资源defer file.Close()防止文件忘记关闭
解锁defer mu.Unlock()防止死锁
记录日志/退出操作defer log.Println("退出")确保函数末尾执行
Done 通知defer wg.Done()保证 goroutine 退出时减少计数
3.2.2 在Goroutine中的使用案例
package mainimport ("fmt""sync"
)var wg sync.WaitGroupfunc hello(i int) {defer wg.Done() // 必须在协程结束时调用 wg.Done() 通知 WaitGroup 协程结束fmt.Println("Hello Goroutine!", i)
}
func main() {for i := 0; i < 10; i++ {wg.Add(1)   // 告诉 WaitGroup 等待 1 个协程go hello(i) // 启动另外一个goroutine去执行hello函数}wg.Wait() // 等待所有的 goroutine 结束fmt.Println("main goroutine done!")
}
3.2.3 多个defer的执行顺序

defer 在 Go 中的执行顺序就是栈结构的“先进后出(LIFO)”原则:

🧠 defer 的执行顺序:后注册,先执行
也就是:

func demo() {defer fmt.Println("1")defer fmt.Println("2")defer fmt.Println("3")
}

🧾 输出结果:

3
2
1

📦 原理解释:
当你写多个 defer 时,Go 会把它们压入一个栈中,函数结束时就从栈顶往下依次执行这些 defer。

行号执行内容进入 defer 栈
1defer fmt.Println("1")栈:[1]
2defer fmt.Println("2")栈:[1, 2]
3defer fmt.Println("3")栈:[1, 2, 3]
return前执行 defer从栈顶开始弹出输出顺序:3 → 2 → 1

✅ 常见用途
多个 defer 通常用于按顺序清理资源,比如:

func handleFile() {f1, _ := os.Open("file1.txt")defer f1.Close() // 最后关闭f2, _ := os.Open("file2.txt")defer f2.Close() // 比 f1 先关闭fmt.Println("doing something...")
}

结果是 file2 会先关闭,然后 file1 再关闭 —— 和你打开文件的顺序是相反的,这正是我们想要的行为(像“关门要倒着来”一样)。

✅ 总结:

特性说明
顺序多个 defer 是“后注册,先执行”(LIFO)
应用场景清理资源(文件、锁、连接等)、错误处理等
安全性即使函数 returnpanic,也会执行

4.主协程和其他协程的关系,主协退出了,其他的协程还执行吗?

代码使用了官网提供的:

package mainimport ("fmt""time"
)func main() {// 合起来写go func() {i := 0for {i++fmt.Printf("new goroutine: i = %d\n", i)time.Sleep(time.Second)}}()i := 0for {i++fmt.Printf("main goroutine: i = %d\n", i)time.Sleep(time.Second)if i == 2 {break}}
}

执行结果:

main goroutine: i = 1
new goroutine: i = 1
main goroutine: i = 2
new goroutine: i = 2
new goroutine: i = 3Process finished with the exit code 0

证明了主协程结束,其他线程不会再执行

5. java/c/c++线程与go协程的对比(与OS线程)

特性Java / C 的线程(OS Thread)Go 的 goroutine
线程类型操作系统线程(内核线程)用户级线程(协程)
线程模型1:1 模型M:N 模型
调度者操作系统Go 自带的调度器(runtime)
映射关系每个语言线程对应一个 OS 线程多个 goroutine 映射到多个 OS 线程
栈内存初始大小通常 1MB~2MB(固定)起始约 2KB(可动态伸缩)
创建成本高(需要系统调用)极低(用户态,几乎无开销)
调度成本高(内核态线程切换)低(用户态线程切换)
并发数量限制一般几千个十万甚至百万级
适合场景计算密集、高性能场景高并发、大量 I/O 场景
常用语言APIstd::thread, Threadgo myFunc()
内存使用效率相对较低非常高

🔍 示例类比:

类比Java / C 的线程Go 的 goroutine
比喻重型卡车:开销大但能干活自行车大军:轻量且灵活
调度员操作系统Go 自己的调度器
数量几千个已很吃力十万个都轻轻松松

✅ 图示说明

Java / C         =>        1:1 线程模型
┌──────────┐         ┌──────────┐
│ Thread A │───────▶│  OS 线程 A │
│ Thread B │───────▶│  OS 线程 B │
└──────────┘         └──────────┘Go              =>        M:N 线程模型
┌──────────────┐
│ goroutine 1  │
│ goroutine 2  │
│ goroutine 3  │──┐
│ goroutine 4  │  │
│ goroutine 5  │  ├──▶ 被 Go runtime 调度
│ goroutine N  │──┘     分配到 OS 线程 A/B/C…
└──────────────┘

✅ 总结一句话:
Java 和 C 的线程就是系统线程(1:1),重量级。
Go 的 goroutine 是用户级线程,轻量可扩展(M:N),适合高并发。

http://www.xdnf.cn/news/12067.html

相关文章:

  • 基于西门子S7-200 PLC、KEPServerEx、sql server2012 的闸门群OPC UA数据采集
  • docker快速部署OS web中间件 数据库 编程应用
  • FPGA点亮ILI9488驱动的SPI+RGB接口LCD显示屏(一)
  • 嵌入式学习之系统编程(十)网络编程之TCP传输控制协议
  • python打卡day45
  • OpenCV 图像通道的分离与合并
  • SpringBoot3项目架构设计与模块解析
  • CIFAR10的使用
  • 【Redis】Redis 的常见客户端汇总
  • 四六级监考《培训学习》+《培训考试》
  • linux 串口调试命令 stty
  • HTML中各种标签的作用
  • 储能数字化的第一步,是把直流能量“看清楚
  • 【Qt】之【Get√】【Bug】通过值捕获(或 const 引用捕获)传进 lambda,会默认复制成 const
  • 二叉树-104.二叉树的最大深度-力扣(LeetCode)
  • (头歌作业)-6.5 幻方(project)
  • 【大模型】MCP是啥?它和点菜、做菜、端菜有啥关系?
  • 【python深度学习】Day 45 Tensorboard使用介绍
  • [蓝桥杯]摆动序列
  • 深度强化学习驱动的智能爬取策略优化:基于网页结构特征的状态表示方法
  • Ubuntu ssh 永久添加私钥
  • Ubuntu ifconfig 查不到ens33网卡
  • 【Android基础回顾】三:Android启动流程
  • 使用Python提取PDF元数据的完整指南
  • 《棒球百科知识》1号位是什么位置·野球1号位
  • 三甲医院“AI平台+专家系统”双轮驱动模式的最新编程方向分析
  • 基于51单片机的天然气浓度检测报警系统
  • 第14节 Node.js 全局对象
  • AI系统微服务架构——服务网关与API网关
  • STM32发送MQTT请求到Onenet