Go语言提供了锁和channel两种核心的并发同步机制,两者都能解决多goroutine访问共享资源的问题,但适用场景存在明显差异,开发者需要根据实际需求选择合适的方案。

Go中锁的基本使用场景
Go的标准库sync包提供了多种锁类型,最常用的是互斥锁sync.Mutex和读写锁sync.RWMutex,锁的核心作用是保护共享资源,避免多个goroutine同时修改资源导致数据不一致。
当多个goroutine需要直接操作同一个共享变量,且操作逻辑比较简单,不需要传递数据时,锁是更合适的选择。比如多个goroutine同时对一个计数器进行增减操作,使用互斥锁就能快速实现同步。
下面是使用互斥锁实现计数器累加的示例:
package main
import (
"fmt"
"sync"
)
// 计数器结构体,包含互斥锁和计数值
type Counter struct {
mu sync.Mutex
value int
}
// 累加方法,操作前加锁,操作后解锁
func (c *Counter) Add(n int) {
c.mu.Lock()
defer c.mu.Unlock()
c.value += n
}
// 获取当前计数值
func (c *Counter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var wg sync.WaitGroup
counter := &Counter{}
// 启动10个goroutine,每个累加100次
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
counter.Add(1)
}
}()
}
wg.Wait()
fmt.Println("最终计数器值:", counter.Get())
}
Go中channel的基本使用场景
channel是Go语言中用于goroutine之间通信的原生类型,遵循不要通过共享内存来通信,而要通过通信来共享内存的设计理念。channel更适合需要传递数据、协调多个goroutine执行流程的场景。
当多个goroutine之间需要传递任务、返回结果,或者需要控制goroutine的执行顺序时,channel是更优的选择。比如生产者消费者模型,生产者goroutine生成数据放入channel,消费者goroutine从channel取出数据进行处理,天然契合channel的设计定位。
下面是使用channel实现生产者消费者模型的示例:
package main
import (
"fmt"
"sync"
)
// 生产者函数,生成数据发送到channel
func producer(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("生产者生产数据:", i)
}
// 生产完成后关闭channel
close(ch)
}
// 消费者函数,从channel接收数据并处理
func consumer(ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for data := range ch {
fmt.Println("消费者消费数据:", data)
}
}
func main() {
var wg sync.WaitGroup
ch := make(chan int, 2)
// 启动1个生产者和2个消费者
wg.Add(3)
go producer(ch, &wg)
go consumer(ch, &wg)
go consumer(ch, &wg)
wg.Wait()
}
Go锁与channel的选择建议
在实际开发中,可以从以下几个维度判断该使用锁还是channel:
- 如果核心需求是保护共享资源,避免并发读写冲突,优先选择锁,尤其是操作逻辑简单、不需要跨goroutine传递数据的场景,锁的实现更轻量,性能开销更小。
- 如果核心需求是goroutine之间的数据传递、流程协调,优先选择channel,符合Go的并发设计哲学,代码可读性和维护性更好。
- 如果共享资源的操作逻辑复杂,需要结合多个步骤的同步,且不需要传递数据,可以选择读写锁提升读多写少场景的性能。
- 如果场景同时需要资源保护和数据传递,可以根据复杂度选择:简单场景可以用锁加channel组合,复杂场景优先用channel封装共享资源的操作,避免锁的滥用。
常见误区提醒
很多开发者会陷入两个极端,要么所有并发场景都用锁,要么所有场景都用channel。实际上两者并不是互斥的,部分场景下可以结合使用。另外需要注意,锁使用时要避免死锁,比如重复加锁、加锁后忘记解锁;channel使用时要避免向已关闭的channel发送数据,或者接收端没有正确处理channel关闭的情况。
Go并发编程中没有绝对正确的选择,核心是根据场景匹配最合适的工具,同时保证代码的清晰性和可维护性。