goroutine本质是加go前缀启动函数,但需同步控制、显式传参、配合channel或WaitGroup;应避免匿名函数立即执行,改用命名函数调用,并警惕循环中闭包捕获变量陷阱。
goroutine 本质就是加个 go 前缀启动函数,但不加同步控制就跑飞、不传参就闭包踩坑、不配 channel 或 sync.WaitGroup 就收不到结果——它极简,也极容易“看似在跑,其实没跑完”。
go func() { ... }(),直接调函数新手常把 goroutine 启动写成匿名函数立即执行的形式,比如:
go func() {
fmt.Println("hello")
}()这没错,但多数时候是画蛇添足。真正该做的是:把逻辑封装成普通函数,用 go 调用它。
✅ 正确姿势:
func say(msg string) {
fmt.Println(msg)
}
func main() {
go say("hello from goroutine")
time.Sleep(10 * time.Millisecond) // 临时保命,实际要用 wait 或 channel
}
下面这段代码几乎 100% 输出全是 3:
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // 全部打印 3!
}()
}原因:所有 goroutine 共享同一个变量 i 的地址,等它们真正执行时,for 早已结束,i == 3。
time.Sleep 拖延i 当参数传进去(推荐)for i := 0; i < 3; i++ {
go func(val int) {
fmt.Println(val) // 输出 0, 1, 2
}(i)
}for i := 0; i < 3; i++ {
i := i // 新建局部变量
go func() {
fmt.Println(i) // 输出 0, 1, 2
}()
}time.Sleep,用 sync.WaitGroup 或 channel
time.Sleep 是调试玩具,生产环境绝对不能用——它既不准(可能等不够或等太久),也不可靠(CPU 负载高时调度延迟变大)。
sync.WaitGroup
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // 阻塞直到所有 Done()
}
chan
c := make(chan int, 3)
go func() { c <- 42 }()
go func() { c <- 100 }()
go func
() { c <- -1 }()
// 收三个数(带缓冲可非阻塞)
fmt.Println(<-c, <-c, <-c)close,别让 range 卡死用 for range ch 接收 channel 数据时,如果没人关 channel,接收方会永远阻塞在 range 上——哪怕所有发送 goroutine 已退出。
ch := make(chan int)
go func() { ch <- 1; ch <- 2 }() // 没 close!
for v := range ch { // 这里卡住,死锁
fmt.Println(v)
}close(ch),且确保只关一次ch := make(chan int)
go func() {
ch <- 1
ch <- 2
close(ch) // 关键!
}()
for v := range ch { // 安全退出
fmt.Println(v)
}真正难的不是语法,而是谁负责关闭、什么时候关、并发关闭是否重复——这些得结合业务生命周期设计,不是加一行 close 就万事大吉。
来电咨询