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(),但真正重要的是 Add 和 Value 的接收者都是 *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,这样它们争用的是同一个锁,而不是各自副本中的锁。

为什么值接收者会破坏这个设计
值接收者的语法看起来更简短,但每次调用都会得到一份接收者副本。对于普通不可变小对象,这可能没有问题;对于包含锁的结构体,副本同时包含一份复制后的锁状态和一份复制后的数据。
// 错误示例: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 的代码路径。

最后可以用四个问题做快速判断:锁是否只服务一个对象?若是,使用值字段;方法是否会改变状态或包含锁?使用指针接收者;对象是否已使用过锁?禁止按值复制;是否真的需要共享锁?只有这时才考虑 *sync.Mutex,并集中初始化。
相关问题
Mutex 可以放在结构体的第一个字段吗?
可以。字段顺序通常不是并发正确性的关键,关键是锁和受保护的数据要由同一组方法一致地访问,并避免复制已经使用过的结构体。
读方法一定要用指针接收者吗?
只要结构体包含 Mutex 或其他不可复制同步状态,就应保持指针接收者,即使方法看起来只是读取。这样可以避免读方法本身复制接收者,也让整个类型的调用约定保持一致。
参考资料
-
245 收藏
-
109 收藏
-
291 收藏
-
417 收藏
-
477 收藏
-
312 收藏
-
436 收藏
-
396 收藏
-
426 收藏
-
152 收藏
-
479 收藏
-
250 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习