goroutine
约 1574 字大约 5 分钟
2026-01-18
goroutine 是 Go 并发模型里最基础的执行单元。它不是线程,但会被 Go runtime 调度到少量线程上执行。在 Go 里,只要在一个函数调用前面加上 go 关键字,这个函数就会以 goroutine 的形式并发执行:
package main
import "fmt"
func sayHello() {
fmt.Println("hello goroutine")
}
func main() {
go sayHello() // 会新建一个 goroutine来执行 sayHello
fmt.Println("main exit")
}重要
main() 本身运行在一个 goroutine 中。如果 main 退出,代表整个程序结束,其他携程也会被强制终止,不管它们是否完成了任务
线程与 goroutine
线程是一个操作系统级别的调度单位,而 goroutine 是 Go runtime 调度单位
| 对比项 | goroutine | 线程 |
|---|---|---|
| 创建成本 | 很低 | 相对高 |
| 栈空间 | 初始很小,按需增长 | 默认更大 |
| 调度方 | Go runtime | 操作系统 |
| 数量规模 | 常常成千上万 | 通常远少于 goroutine |
| 切换成本 | 更低,很多发生在用户态 | 更高,依赖内核调度 |
基本使用
启动多个 goroutine
go runtime 不会限制 goroutine 的数量,但实际项目里要根据任务特点合理控制并发度
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Println("worker start:", id)
time.Sleep(500 * time.Millisecond)
fmt.Println("worker done:", id)
}
func main() {
// 使用循环开启 3 个 goroutine 执行 worker 函数
for i := 1; i <= 3; i++ {
go worker(i)
}
time.Sleep(1 * time.Second)
}警告
goroutine 的执行顺序没有保证。goroutine 由 Go 运行时调度器统一调度,多个 goroutine 何时开始执行、谁先执行、谁后执行,通常是不确定的
因此,像上面这样在循环中启动多个 goroutine 时,worker start 输出的 id 顺序不能依赖创建顺序
WaitGroup
waitGroup 是 Go 标准库 sync 包提供的一个工具,用于等待一组 goroutine 完成。在某些情况下,需要等待一批 goroutine 执行完毕后再继续执行后续代码;或者需要确保所有 goroutine 都完成了某些工作后才退出程序,此时 WaitGroup 就非常有用
| 方法 | 作用 |
|---|---|
Add(n int) | 增加等待的 goroutine 数量 |
Done() | 表示一个 goroutine 完成了工作,等待数量减一 |
Wait() | 阻塞当前 goroutine,直到等待数量为零 |
// case: 主函数里开启了多个 goroutine 做任务,如果不等它们执行完,主程序可能直接退出
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("worker start:", id)
time.Sleep(1 * time.Second)
fmt.Println("worker done:", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有 goroutine 执行完成
wg.Wait()
fmt.Println("all workers finished")
}package main
import (
"fmt"
"sync"
"time"
)
func handleTask(taskID int, results *[]string, mu *sync.Mutex, wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(500 * time.Millisecond)
result := fmt.Sprintf("task %d done", taskID)
// 多个 goroutine 共享写入结果,需要加锁保护
mu.Lock()
*results = append(*results, result)
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
tasks := []int{101, 102, 103, 104, 105}
results := make([]string, 0, len(tasks))
for _, taskID := range tasks {
wg.Add(1)
go handleTask(taskID, &results, &mu, &wg)
}
// 等待所有任务处理完成
wg.Wait()
fmt.Println("all tasks finished")
fmt.Println("results:")
for _, result := range results {
fmt.Println(result)
}
}goroutine 如何返回结果
goroutine 不能像普通函数那样直接返回值给调用方。
通常有三种做法:
| 方式 | 适用场景 | 说明 |
|---|---|---|
channel | 最常见 | 用于结果传递、错误传递、同步 |
| 共享变量 + 锁 | 多 goroutine 共享状态 | 需要自己保证并发安全 |
| 回调 / 闭包写入外部对象 | 特定框架或任务模型 | 可读性和同步控制要额外注意 |
goroutine 结果回传方式
最典型的是通过 channel 返回结果:
package main
import "fmt"
func main() {
resultCh := make(chan int)
go func() {
resultCh <- 1 + 2
}()
result := <-resultCh
fmt.Println(result)
}goroutine 很轻量,轻量在哪里
很多人会背“goroutine 很轻量”,但说不清为什么。
可以从这几个点理解:
- 初始栈空间很小,不像线程那样一上来就占很大内存
- 栈会按需增长,不需要提前给很大空间
- 调度主要由 Go runtime 管理,不完全依赖内核线程调度
- 一个线程可以复用执行大量 goroutine
这里的“轻量”不等于“免费”
goroutine 虽然便宜,但不是没有成本:
- 创建太多 goroutine 仍然会占用内存
- goroutine 泄漏会导致资源长期不释放
- 大量无意义并发还会增加调度开销
所以正确的说法不是“goroutine 可以随便开”,而是“goroutine 的单位成本比线程低很多”。
常见控制方式
goroutine 本身只是“并发执行”的载体,实际项目里通常还要配合这些能力:
| 配套机制 | 作用 |
|---|---|
channel | 传递数据、做同步 |
select | 多路等待、超时控制 |
context | 取消、超时、链路控制 |
sync.WaitGroup | 等待一组 goroutine 结束 |
sync.Mutex / sync.RWMutex | 保护共享状态 |
goroutine 常见配套机制
常见陷阱
main 提前退出
这是最常见的问题:
func main() {
go func() {
fmt.Println("never maybe printed")
}()
}main 退出后,程序立即结束,子 goroutine 不会被“保活”。
goroutine 泄漏
goroutine 泄漏的本质是:
goroutine 还活着,但永远等不到结束条件。
典型例子:
func main() {
ch := make(chan int)
go func() {
<-ch // 一直等
}()
}如果这个 ch 永远没人写入,也没人关闭,这个 goroutine 就会一直挂着。
闭包变量使用不当
在并发循环里,闭包引用循环变量是经典坑点。安全写法是把变量作为参数传进去:
for i := 0; i < 3; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}注
新版本 Go 对 for range 变量捕获做过改进,但“显式传参”的写法仍然最清晰,也最不容易误用。
什么时候应该用 goroutine
适合:
- I/O 并发任务,例如请求多个接口、处理连接、异步读写
- 可以拆成独立子任务的工作
- 需要把耗时操作从主流程中解耦
不适合:
- 只是为了“看起来并发”,但任务非常小、并发反而引入更多调度开销
- 大量共享状态却没有设计好同步方式
- 没有退出机制,容易造成 goroutine 泄漏
