goroutine是Go语言并发设计的核心。它是一种轻量级的线程,由Go运行时管理,创建成本极低(初始栈仅几KB)。通过在函数调用前加上go关键字,就可以启动一个并发的goroutine。借助goroutine和channel,Go能够轻松处理高并发场景。
go关键字即可启动,语言原生支持。
只需在函数调用前添加go关键字。下面的示例会同时打印“hello”和“world”,但顺序不确定。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world") // 启动新goroutine
say("hello") // 主goroutine继续运行
}
输出可能交错(如“hello world hello world hello world”)。由于主函数返回时会立即终止所有goroutine,因此可能需要等待(后面介绍WaitGroup)。
func main() {
go func() {
fmt.Println("匿名goroutine执行")
}()
time.Sleep(time.Millisecond) // 简单等待(不推荐)
}
注意:匿名goroutine如果捕获外部变量,要注意闭包特性(可能共享变量)。
time.Sleep不适合生产环境。标准库sync.WaitGroup提供了可靠的等待机制。
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 完成时计数器减1
fmt.Printf("Worker %d 开始工作\n", id)
// 模拟工作
fmt.Printf("Worker %d 完成\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 计数器加1
go worker(i, &wg) // 传递WaitGroup地址
}
wg.Wait() // 阻塞直到计数器归零
fmt.Println("所有worker完成")
}
wg.Add必须在goroutine启动前调用,且wg.Done最好使用defer确保即使出错也能减计数器。// 示例:启动大量goroutine(不会立即耗尽内存)
var wg sync.WaitGroup
for i := 0; i < 100000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
time.Sleep(time.Millisecond)
}(i)
}
wg.Wait()
Go调度器采用GMP模型:G(goroutine)、M(操作系统线程)、P(处理器/上下文)。P的数量由GOMAXPROCS决定,默认为CPU核心数。调度器将G分配到M上执行,实现多路复用。
可以通过runtime.GOMAXPROCS(n)设置并行度。
多个goroutine并发访问同一变量可能导致不可预知的结果。下面的示例存在数据竞争。
var counter int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // 非原子操作,存在竞态
}()
}
wg.Wait()
fmt.Println(counter) // 通常小于1000
sync.Mutex、atomic包或channel。具体将在下一章详细介绍。通常使用channel传递退出信号,或用context.Context。
func worker(stop <-chan struct{}) {
for {
select {
case <-stop:
fmt.Println("收到退出信号")
return
default:
// 正常工作
}
}
}
package main
import (
"fmt"
"sync"
"time"
)
type Task struct {
ID int
Name string
}
func process(task Task, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("开始处理任务: %d - %s\n", task.ID, task.Name)
time.Sleep(time.Millisecond * 500) // 模拟耗时
fmt.Printf("完成任务: %d\n", task.ID)
}
func main() {
tasks := []Task{
{1, "数据清洗"},
{2, "数据分析"},
{3, "生成报告"},
}
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go process(task, &wg)
}
wg.Wait()
fmt.Println("所有任务处理完成")
}
go关键字即可启动,配合sync.WaitGroup可以协调多个goroutine的完成。但要注意数据竞态和生命周期管理。下一章我们将学习goroutine之间的通信方式——channel。