登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go sync.Mutex 应该嵌入值还是通过指针接收者使用

来源:17golang原创

时间:2026-09-09 10:39:08 443浏览 收藏

如果一个 Go 结构体内部有 sync.Mutex,常见且稳妥的组合是:把锁作为值字段嵌入,把结构体的方法写成指针接收者。也就是 sync.Mutex 负责保存这一个对象的锁状态,*Counter 负责让方法始终修改同一个对象,而不是复制出一个新接收者。

结论很简单:优先使用 sync.Mutex 值字段 + 指针接收者;不要用值接收者操作包含锁的结构体,也不要为了“避免复制”盲目把锁改成 *sync.Mutex
要点速览
  • sync.Mutex 的零值可直接使用,通常不需要 new 或构造函数。
  • 包含 Mutex 的结构体方法使用指针接收者,避免调用时复制锁和受保护的数据。
  • 结构体一旦开始使用锁,就不要再按值传参、赋值、返回或放入会复制它的容器。

锁按值嵌入,方法按指针接收者

下面的写法把锁和计数值放在同一个对象里。匿名嵌入让方法可以写成 c.Lock(),但真正重要的是 AddValue 的接收者都是 *Counter

package main

import "sync"

// Counter 把计数值和保护它的锁绑定在同一个对象中。
type Counter struct {
    sync.Mutex
    value int
}

// Add 必须用指针接收者,避免复制 Mutex 和 value。
func (c *Counter) Add(delta int) {
    c.Lock()
    defer c.Unlock() // 无论函数如何返回,都释放临界区
    c.value += delta
}

// Value 也用指针接收者,读取和写入使用同一把锁。
func (c *Counter) Value() int {
    c.Lock()
    defer c.Unlock()
    return c.value
}

声明 var c Counter 后,锁的零值就是未加锁状态,可以直接调用方法。多个 goroutine 应共享同一个 *Counter,这样它们争用的是同一个锁,而不是各自副本中的锁。

值嵌入 Mutex 与指针接收者共同保护同一个 Counter 对象的结构关系
图1:值字段保存 Mutex,指针接收者让业务方法始终落在同一个 Counter 对象上。

为什么值接收者会破坏这个设计

值接收者的语法看起来更简短,但每次调用都会得到一份接收者副本。对于普通不可变小对象,这可能没有问题;对于包含锁的结构体,副本同时包含一份复制后的锁状态和一份复制后的数据。

// 错误示例:c 是 Counter 的副本,不是调用者手里的原对象。
func (c Counter) Add(delta int) {
    c.Lock()
    defer c.Unlock()
    c.value += delta // 改的是副本,调用者看不到这次修改
}

func use() {
    var c Counter
    c.Add(1) // 编译器可以取得 c 的地址,但方法声明仍会复制值
}

这里有两个问题:第一,value 的修改写入副本;第二,锁也被复制,调用者和方法内部可能并没有用同一把锁保护同一份数据。官方文档要求 Mutex 首次使用后不得复制,Go 的代码审查建议也明确指出,含有同步字段的结构体应使用指针接收者。

把锁写成 *sync.Mutex 就一定更好吗

不一定。指针字段能让多个结构体显式共享一把锁,但也引入了锁的初始化和生命周期问题:

写法默认状态主要风险适用判断
sync.Mutex零值可用对象使用后不能复制对象自己拥有并保护自己的状态,优先选择
*sync.Mutex默认是 nil忘记初始化会 panic;复制指针会共享锁确实需要外部注入或多个对象共享同一锁时再用

如果选择指针字段,初始化应当集中处理,不能让调用方猜测锁是否为空:

type SharedCounter struct {
    mu    *sync.Mutex
    value int
}

func NewSharedCounter() *SharedCounter {
    return &SharedCounter{
        mu: new(sync.Mutex), // 明确创建锁的所有权
    }
}

func (c *SharedCounter) Add(delta int) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value += delta
}

这个版本只有在“锁需要被注入或共享”时才有价值。若锁和数据天然属于同一个对象,值字段更容易保证零值可用,也不容易出现 nil 锁。

实际项目中重点检查哪些复制路径

除了值接收者,还要留意函数参数、返回值、结构体赋值和把对象放进接口时的复制。一个已经加过锁的 Counter 不应再写成 func f(c Counter),也不应通过 c2 := c1 制造第二份值。更安全的边界是传递 *Counter,并让对象的所有权和并发访问方式保持清晰。

func update(c *Counter) {
    c.Add(1)
}

func main() {
    c := &Counter{}
    update(c) // 共享同一对象和同一把锁
}

// go vet -copylocks ./...
// 用静态检查尽早发现可能复制 Mutex 的代码路径。
复制包含 Mutex 的结构体会形成副本锁,指针传递则继续使用同一对象
图2:比较按值复制与传递指针的两条路径,判断锁和被保护数据是否仍然属于同一对象。

最后可以用四个问题做快速判断:锁是否只服务一个对象?若是,使用值字段;方法是否会改变状态或包含锁?使用指针接收者;对象是否已使用过锁?禁止按值复制;是否真的需要共享锁?只有这时才考虑 *sync.Mutex,并集中初始化。

相关问题

Mutex 可以放在结构体的第一个字段吗?

可以。字段顺序通常不是并发正确性的关键,关键是锁和受保护的数据要由同一组方法一致地访问,并避免复制已经使用过的结构体。

读方法一定要用指针接收者吗?

只要结构体包含 Mutex 或其他不可复制同步状态,就应保持指针接收者,即使方法看起来只是读取。这样可以避免读方法本身复制接收者,也让整个类型的调用约定保持一致。

参考资料

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>