Go 语言自诞生以来,其并发模型一直是吸引开发者的核心特性之一。与传统的线程模型不同,Go 提供了 goroutine 和 channel 这两个轻量级原语,让并发编程变得简洁而安全。本文将深入探讨 Go 并发编程的实战技巧,从基础概念到高级设计模式,帮助你写出高效、可维护的并发代码。
Goroutine 是 Go 并发的基础,它的开销极小(初始栈大小仅 2KB),可以在一个进程中同时运行数十万个 goroutine。
启动一个 goroutine 只需在函数调用前加上 go 关键字:
go func() {
fmt.Println("Hello from goroutine")
}()
但实际开发中,我们通常需要等待 goroutine 完成。最简单的做法是使用 sync.WaitGroup:
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait()
}
在循环中启动 goroutine 时,必须注意变量捕获问题:
// 错误示例:所有 goroutine 都可能打印同一个值
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i) // i 在 goroutine 执行时可能已改变
}()
}
// 正确做法:通过参数传递
for i := 0; i < 10; i++ {
go func(id int) {
fmt.Println(id)
}(i)
}
Channel 是 Go 中 goroutine 通信的主要方式,遵循"通过通信共享内存,而非通过共享内存通信"的设计哲学。
// 无缓冲 channel(同步通信)
ch := make(chan int)
// 有缓冲 channel(异步通信)
buffered := make(chan string, 10)
// 只读/只写 channel 作为函数参数
func producer(out chan<- int) { ... } // 只能写入
func consumer(in <-chan int) { ... } // 只能读取
select 语句让一个 goroutine 可以同时等待多个 channel 操作:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
case <-time.After(3 * time.Second):
fmt.Println("Timeout")
}
}
将一个生产者的任务分发给多个 worker 处理,再汇总结果:
func producer(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func worker(id int, in <-chan int, out chan<- int) {
for n := range in {
out <- n * n // 模拟处理
time.Sleep(100 * time.Millisecond)
}
}
func fanOut(in <-chan int, workerCount int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for n := range in {
out <- n * n
}
}(i)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
nums := producer(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
results := fanOut(nums, 3)
for r := range results {
fmt.Println(r)
}
}
将数据处理分为多个阶段,每个阶段通过 channel 连接:
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func filterOdd(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if n%2 == 0 {
out <- n
}
}
close(out)
}()
return out
}
func main() {
// 构建 pipeline: gen -> square -> filterOdd
result := filterOdd(square(gen(1, 2, 3, 4, 5, 6)))
for r := range result {
fmt.Println(r) // 输出: 4, 16, 36
}
}
控制并发度,避免系统资源耗尽:
type Job struct {
ID int
Data string
}
type Result struct {
JobID int
Output string
}
func worker(id int, jobs <-chan Job, results chan<- Result) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job.ID)
time.Sleep(time.Second) // 模拟耗时操作
results <- Result{JobID: job.ID, Output: fmt.Sprintf("Processed: %s", job.Data)}
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan Job, numJobs)
results := make(chan Result, numJobs)
// 启动 worker
for w := 1; w <= numWorkers; w++ {
go worker(w, jobs, results)
}
// 发送任务
for j := 1; j <= numJobs; j++ {
jobs <- Job{ID: j, Data: fmt.Sprintf("data-%d", j)}
}
close(jobs)
// 收集结果
for r := 1; r <= numJobs; r++ {
<-results
}
}
使用 context.Context 实现超时控制和优雅取消:
func slowOperation(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := slowOperation(ctx)
if err != nil {
fmt.Printf("Operation failed: %v\n", err)
// 输出: Operation failed: context deadline exceeded
}
}
并发代码的错误处理比同步代码复杂得多。以下是几种常见策略:
g, ctx := errgroup.WithContext(context.Background())
urls := []string{
"https://example.com",
"https://google.com",
"https://invalid-url",
}
for _, url := range urls {
url := url
g.Go(func() error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
return nil
})
}
if err := g.Wait(); err != nil {
fmt.Printf("Error group failed: %v\n", err)
}
type ResultWithError[T any] struct {
Value T
Err error
}
func fetchAll(urls []string) <-chan ResultWithError[string] {
out := make(chan ResultWithError[string], len(urls))
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
out <- ResultWithError[string]{Err: err}
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
out <- ResultWithError[string]{Value: string(body)}
}(url)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
合理设置 channel 缓冲区大小:有缓冲 channel 可以减少 goroutine 阻塞,但过大的缓冲区会导致内存浪费。建议先压测确定合适的值。
避免 goroutine 泄漏:确保每个 goroutine 最终都能退出。使用 context.Context 或 select 配合 done channel 来控制生命周期。
使用 sync.Pool 复用对象:在高并发场景下,减少对象分配可以显著降低 GC 压力:
var pool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func process(data []byte) {
buf := pool.Get().(*bytes.Buffer)
defer pool.Put(buf)
buf.Reset()
buf.Write(data)
// 使用 buf...
}
GOMAXPROCS 调优:默认为 CPU 核心数,在某些 I/O 密集型场景下适当提高可以提升吞吐量。
Go 的并发模型以简洁著称,但要在实际项目中用好它,需要理解 goroutine 和 channel 的特性,掌握常见的并发设计模式,并注意错误处理和资源管理。本文介绍的 Fan-Out/Fan-In、Pipeline、Worker Pool 和 Context 取消等模式是实际项目中最常用的并发模式,掌握它们能让你写出高效、健壮的 Go 并发程序。
并发编程没有银弹,Go 提供的工具虽然简单,但组合起来可以实现非常复杂的并发逻辑。建议在实际项目中多加练习,逐步积累经验。