
本文详解 go 中 goroutine 并发执行的常见误区(如未等待协程结束、局部 mutex 无效、defer 错误解锁),并提供可运行的修复方案,帮助开发者写出线程安全、行为可预期的并发程序。
本文详解 go 中 goroutine 并发执行的常见误区(如未等待协程结束、局部 mutex 无效、defer 错误解锁),并提供可运行的修复方案,帮助开发者写出线程安全、行为可预期的并发程序。
Go 的并发模型基于 goroutine(轻量级协程)而非传统多线程,但其正确使用需严格遵循同步与生命周期管理原则。原代码存在多个关键错误,导致看似“启动了 4 个 goroutine”,实则主函数立即退出、协程被强制终止,且共享资源(文件)未被真正保护——结果不可预测、输出混乱。
? 主要问题剖析
- 缺少同步等待机制:main() 启动 goroutine 后直接结束,OS 强制终止所有子 goroutine,根本无机会执行完成。
- Mutex 定义位置错误:var fileMutex sync.Mutex 在 test() 函数内声明 → 每次调用都新建一个互斥锁,完全无法跨 goroutine 互斥,形同虚设。
- defer Unlock() 误用:defer 在函数返回时才执行,而循环中需每次 Lock() 后立即 Unlock();当前写法会导致首次 Lock() 后永远不释放,后续 goroutine 全部阻塞或 panic。
- runtime.Gosched() 冗余:该调用主动让出 CPU 时间片,现代 Go 调度器已高度优化,手动调度既无必要又干扰正常调度逻辑。
- 数据结构冗余:[105]int 数组未实际使用,且硬编码范围应由逻辑驱动,建议用 for i := 0; i < 105; i++ 更清晰。
✅ 正确实现:WaitGroup + 全局 Mutex + 显式加锁/解锁
以下为修复后的完整可运行示例,支持 4 个 goroutine 并发写入同一文件,并保证每次写入原子性(避免内容交错):
package main
import (
"fmt"
"log"
"os"
"sync"
)
var wg sync.WaitGroup
var fileMutex sync.Mutex // ✅ 全局唯一,所有 goroutine 共享
func test(s string, fo *os.File) {
defer wg.Done() // ✅ 协程结束时通知 WaitGroup
for i := 0; i < 105; i++ {
fileMutex.Lock() // ✅ 进入临界区前加锁
_, err := fmt.Fprintf(fo, "%s%d", s, i) // 注意:Fprintf 返回 (n int, err error),建议检查 err
if err != nil {
log.Printf("write error: %v", err)
}
fileMutex.Unlock() // ✅ 立即解锁,绝不 defer
}
}
func main() {
fo, err := os.Create("D:/output.txt")
if err != nil {
log.Fatal("failed to create file:", err)
}
defer fo.Close() // ✅ 确保文件句柄及时释放
// 启动 4 个 goroutine,每个写入 "bye0", "bye1", ... "bye104"
for i := 0; i < 4; i++ {
wg.Add(1)
go test("bye", fo)
}
wg.Wait() // ✅ 阻塞 main,等待所有 goroutine 完成
fmt.Println("All goroutines finished. Check D:/output.txt.")
}⚠️ 重要注意事项
- 顺序 ≠ 并发目标:上述代码能安全并发写入,但输出仍是 "bye0bye0bye0bye0bye1bye1..."(4 个 "bye0" 连续出现),因为每个 goroutine 独立执行完整循环。若你期望 "good0bye0good1bye1..." 这类严格交替顺序,本质上不是并发问题,而是协调问题——需引入 channel 或计数器协调轮转,此时并发反而增加复杂度,通常应由单 goroutine 顺序生成更简洁高效。
- I/O 性能考量:高频小写入+Mutex 会严重抵消并发收益。生产环境推荐:各 goroutine 写入独立缓冲(如 bytes.Buffer),最后合并;或批量写入+bufio.Writer 提升吞吐。
- 错误处理不可省略:fmt.Fprintf 可能失败(如磁盘满),忽略错误将导致静默丢失数据。
- 资源清理:务必 defer fo.Close(),避免文件句柄泄漏。
✅ 总结
Go 的并发强大而简洁,但必须尊重其运行时语义:
✅ 用 sync.WaitGroup 等待 goroutine 结束;
✅ 将 sync.Mutex 声明为包级/全局变量以实现跨 goroutine 互斥;
✅ Lock()/Unlock() 成对出现在同一作用域,禁用 defer Unlock();
✅ 移除无意义的 runtime.Gosched();
✅ 优先考虑业务逻辑是否真需并发——顺序任务强行并发常得不偿失。
掌握这些原则,你就能写出稳定、高效、符合 Go 风格的并发程序。

















