goroutine

什么是 goroutine

goroutine 是 Go 运行时管理的轻量级执行单元,类似线程但成本极低:初始栈仅 2KB(线程通常为 MB 级),创建与切换均由运行时调度器完成,单个进程可轻松承载百万级 goroutine。

创建 goroutine

在函数调用前添加 go 关键字即可并发执行:

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 3; i++ {
        fmt.Println(s)
    }
}

func main() {
    go say("world") // 并发执行,立即返回
    say("hello")    // 主 goroutine 继续执行

    time.Sleep(time.Second) // 仅作演示:等待子 goroutine 完成
}

go 语句立即返回,不等待函数结束。若 main 先退出,所有子 goroutine 会被强制终止——这也是上例需要 Sleep 的原因。生产代码不使用 Sleep 同步,而是使用 WaitGroup 或 channel。

sync.WaitGroup:等待一组 goroutine

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 完成时计数减一
    fmt.Printf("worker %d 开始\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)      // 启动前计数加一
        go worker(i, &wg)
    }

    wg.Wait() // 阻塞直至计数归零
    fmt.Println("所有 worker 完成")
}
Tip

WaitGroup 必须以指针传递(内部含计数器状态)。wg.Add(1) 应在启动 goroutine 之前调用,wg.Done()defer 保证执行。

Go 1.25 引入 wg.Go(func(){...}) 方法,将 Add/Done 封装为一行调用。

匿名 goroutine 与参数捕获

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(n int) { // 通过参数传入循环变量
            defer wg.Done()
            fmt.Println("处理", n)
        }(i)
    }
    wg.Wait()
}
Warning

Go 1.22 起,for range 循环变量每次迭代均为新变量,闭包直接捕获是安全的;Go 1.21 及更早版本中,所有迭代共享同一变量,必须像上例那样通过函数参数显式传入,否则存在数据竞争。

goroutine 不是免费的

尽管创建成本极低,仍需管理其生命周期。无法退出、永久阻塞的 goroutine 会造成泄漏,典型诱因包括:

  • 向无人接收的 channel 发送数据。
  • 从已关闭且无数据的 channel 接收数据。
  • 忘记取消带循环的 goroutine(应配合 context 处理)。

并发模式:批量并行处理

package main

import (
    "fmt"
    "sync"
)

func fetch(url string) string {
    return fmt.Sprintf("响应来自 %s", url)
}

func main() {
    urls := []string{"a.com", "b.com", "c.com", "d.com"}

    results := make([]string, len(urls))
    var wg sync.WaitGroup

    for i, u := range urls {
        wg.Add(1)
        go func(idx int, url string) {
            defer wg.Done()
            results[idx] = fetch(url) // 每个索引只被一个 goroutine 写入,安全
        }(i, u)
    }
    wg.Wait()

    for _, r := range results {
        fmt.Println(r)
    }
}
Tip

上例中每个 goroutine 只写自己对应的下标,属于并发安全写法。若多个 goroutine 写同一元素或同一 map,则需要 channel 或 sync.Mutex

小结

  • go 关键字启动轻量协程,调用方需自行管理等待与退出。
  • sync.WaitGroup 是等待一批任务的标准工具。
  • 循环变量捕获注意 Go 版本差异;goroutine 泄漏需用 context 与 channel 设计规避。