# Go语言并发编程的5个核心技巧
## 引言
在当今高性能计算和分布式系统领域,并发编程能力已成为开发者必备技能。Go语言自诞生起就以其出色的并发模型著称,通过goroutine和channel等原生支持,让并发编程变得简单而高效。本文将深入探讨Go语言并发编程的5个核心技巧,帮助开发者写出更安全、高效的并发代码。
## 1. 合理使用goroutine而非线程
传统语言中,线程创建和上下文切换开销巨大,而Go语言的goroutine则轻量得多。
**技巧实践:**
```go
func process(item string) {
// 模拟耗时处理
time.Sleep(100 * time.Millisecond)
fmt.Println("Processed:", item)
}
func main() {
items := []string{"item1", "item2", "item3", "item4"}
// 错误示范:顺序处理
for _, item := range items {
process(item) // 串行执行,耗时约400ms
}
// 正确示范:并发处理
for _, item := range items {
go process(item) // 并行执行,耗时约100ms
}
time.Sleep(1 * time.Second) // 等待所有goroutine完成
}
```
**关键点:**
- 每个goroutine初始栈大小仅2KB,远小于线程MB级的栈
- 调度由Go运行时管理,非操作系统,切换成本低
- 注意避免无限制创建goroutine,可配合worker pool模式
## 2. 善用channel进行协程间通信
Channel是Go语言并发模型的核心,遵循"不要通过共享内存来通信,而应该通过通信来共享内存"的原则。
**基础用法:**
```go
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "processing job", j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动3个worker
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送9个任务
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// 收集结果
for a := 1; a <= 9; a++ {
<-results
}
}
```
**高级技巧:**
- 使用带缓冲的channel提高吞吐量
- 使用select实现多路复用
- 通过关闭channel广播信号
## 3. 正确使用sync包中的同步原语
虽然channel能解决大部分问题,但sync包中的同步原语在特定场景下更高效。
**Mutex使用示例:**
```go
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.v[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
```
**WaitGroup使用示例:**
```go
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // 等待所有worker完成
}
```
## 4. 使用context实现并发控制
Context是Go中管理goroutine生命周期的标准方式,特别适合处理请求超时、取消等场景。
**典型应用场景:**
```go
func operation1(ctx context.Context) error {
time.Sleep(100 * time.Millisecond)
return errors.New("failed")
}
func operation2(ctx context.Context) {
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("done")
case <-ctx.Done():
fmt.Println("halted operation2")
}
}
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go func() {
err := operation1(ctx)
if err != nil {
cancel() // 如果operation1失败,取消所有操作
}
}()
operation2(ctx) // operation2会被取消
}
```
**关键点:**
- 使用context传递取消信号和超时
- 通过WithCancel、WithTimeout等派生context
- 在阻塞操作前检查ctx.Done()
## 5. 避免常见并发陷阱
即使有了好的工具,并发编程仍容易犯错,以下是一些常见陷阱及规避方法。
**陷阱1:数据竞争**
```go
var counter int
func increment() {
counter++ // 并发执行会导致数据竞争
}
// 解决方案:使用互斥锁或原子操作
var counter int64
func incrementSafe() {
atomic.AddInt64(&counter, 1)
}
```
**陷阱2:goroutine泄漏**
```go
func process(ch chan int) {
// 如果goroutine因某些原因阻塞,且channel无人读取
// 这将导致goroutine永远无法退出
val := <-ch
fmt.Println(val)
}
// 解决方案:使用context或设置超时
```
**陷阱3:过度并发**
```go
// 盲目创建大量goroutine可能导致系统资源耗尽
for i := 0; i < 1000000; i++ {
go process() // 可能导致系统崩溃
}
// 解决方案:使用worker pool限制并发数
```
## 结语
Go语言的并发模型是其最强大的特性之一,掌握这些核心技巧能让开发者编写出既高效又安全的并发代码。记住,并发编程的关键在于:
1. 理解goroutine的轻量级特性
2. 正确使用channel进行通信
3. 在适当场景应用同步原语
4. 善用context管理生命周期
5. 时刻警惕并发陷阱
希望这些技巧能帮助你在Go并发编程的道路上更进一步。如果你有更多Go语言并发编程的经验或问题,欢迎在评论区分享讨论!