您现在的位置是:亿华云 > 系统运维
使用Go语言时,谨防锁拷贝!
亿华云2025-10-04 03:46:32【系统运维】4人已围观
简介本文转载自微信公众号「Golang来啦」,作者Seekload。转载本文请联系Golang来啦公众号。四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!相信大家对 Go 语言的锁拷贝问题并不陌生,
本文转载自微信公众号「Golang来啦」,使用时谨作者Seekload。防锁转载本文请联系Golang来啦公众号。拷贝
四哥水平有限,使用时谨如有翻译或理解错误,防锁烦请帮忙指出,拷贝感谢!
相信大家对 Go 语言的使用时谨锁拷贝问题并不陌生,那我们应该如何规范使用Go 语言才能规避这个问题呢?防锁一起来看作者是如何处理的。
原文如下:
假设我们有一个包含 map 的拷贝结构体,现在想在方法中修改这个 map,使用时谨看下面的防锁例子[1]:
package main import "fmt" type Container struct { counters map[string]int } func (c Container) inc(name string) { c.counters[name]++ } func main() { c := Container{ counters: map[string]int{ "a": 0, "b": 0}} doIncrement := func(name string, n int) { for i := 0; i < n; i++ { c.inc(name) } } doIncrement("a", 100000) fmt.Println(c.counters) }Container 包含一个计数器集合,按 name 区分。拷贝inc() 会按 name 对相应的使用时谨计数器执行自增操作(假设计数器存在)。main() 里循环多次调用 inc()。防锁
执行上面的拷贝代码,输出:
map[a:100000 b:0]现在假设有两个 goroutine 会并发地调用 inc()。因为我们必须小心竞争条件,所以使用了 Mutex 保护临界区。
package main import ( "fmt" "sync" "time" ) type Container struct { sync.Mutex // <-- Added a mutex counters map[string]int } func (c Container) inc(name string) { c.Lock() // <-- Added locking of the mutex defer c.Unlock() c.counters[name]++ } func main() { c := Container{ counters: map[string]int{ "a": 0, "b": 0}} doIncrement := func(name string, n int) { for i := 0; i < n; i++ { c.inc(name) } } go doIncrement("a", 100000) go doIncrement("a", 100000) // Wait a bit for the goroutines to finish time.Sleep(300 * time.Millisecond) fmt.Println(c.counters) }你期望上面这段代码会输出什么呢?我得到的结果是这样的:
func (c *Container) inc(name string) { c.Lock() defer c.Unlock() c.counters[name]++ }我们使用 mutex 时已经很小心了,怎么还会出问题呢?你觉得应该如何修复这个问题?提示:只需要改动一个字符的代码就可以了!
代码的问题在于,无论何时调用 inc(),c 都会是亿华云计算一份拷贝,因为 inc() 是定义在 Container 上,而非 *Container;换句话说,c 是值接受者,而不是指针接受者。因此,inc() 并不能真正修改 c 的内容。
但等等,文章第一个示例是如何工作的?在单协程的例子中,c 也是按值传递,但是为什么能得到正确的结果 -- 在 inc() 在对 map 所做的修改,能影响到 main() 函数的原始值。这是因为 map 是引用类型而非值类型。Container 里保存的是指向 map 的指针,而不是 map 实际的站群服务器数据。所以即使我们创建 Container 的副本,counters 保存的仍是指向 map 的地址。
所以文章第一个例子也是存在问题的,尽管执行结果没有问题,但是使用方法不符合官方指南[2] - 在方法中对原始数据进行修改,则方法应定义成指针方法,而非值方法。这里对 map 的使用给了我们一种错误的提示。作为练习,可以将第一个示例中的 map 换成 int 类型的计数器,并注意观察 inc() 的副本是如何递增的,在 inc() 中对副本做的修改不会影响到 main() 中的原始值。
Mutex 是值类型(可以看 Go 文档[3]相关的定义,包括注释里也明确地提示不能拷贝),源码库复制再使用是错误的。复制仅仅是创建了一个新的 mutex,很显然地,对计数器的互斥使用就失效了。
所以应该这样修改,定义 inc() 方法时在 Container 之前添加 *:
func (c *Container) inc(name string) { c.Lock() defer c.Unlock() c.counters[name]++ }c 通过指针方式传到方法中,指向的 Container 与 main() 函数里面的是同一个。
这个问题并不罕见,事实上,使用 go vet 命令就会发现这个问题:
$ go tool vet method-mutex-value-receiver.go method-mutex-value-receiver.go:19: inc passes lock by value: main.Container在我看来,实际上这个问题帮助我们理清了值接收者与指针接收者之间的区别。为了说明这一点,下面还有一个示例,这个示例与上面两个示例没有关系。这个示例使用到了 & 取值符和 %p 格式化输出变量的地址。
package main import "fmt" type Container struct { i int s string } func (c Container) byValMethod() { fmt.Printf("byValMethod got &c=%p, &(c.s)=%p\n", &c, &(c.s)) } func (c *Container) byPtrMethod() { fmt.Printf("byPtrMethod got &c=%p, &(c.s)=%p\n", c, &(c.s)) } func main() { var c Container fmt.Printf("in main &c=%p, &(c.s)=%p\n", &c, &(c.s)) c.byValMethod() c.byPtrMethod() }执行代码后输出(如果在你的机器上执行,输出的地址可能不同,但是这不影响说明问题):
in main &c=0xc00000a060, &(c.s)=0xc00000a068 byValMethod got &c=0xc00000a080, &(c.s)=0xc00000a088 byPtrMethod got &c=0xc00000a060, &(c.s)=0xc00000a068main() 函数里创建了 Container 变量 c,并且输出它的地址和它的成员 s 的地址,接着调用了 Container 的两个方法。byValMethod() 是值接受者,因为是原值的拷贝所有打印的地址不一样。另一方面,byPtrMethod() 是指针接收者,输出的地址与 main() 函数输出的地址一致,因为调用时获取的是 c 实际的地址,而不是副本。
参考资料
[1]例子: https://github.com/eliben/code-for-blog/tree/master/2018/go-copying-mutex
[2]官方指南: https://golang.org/doc/faq#methods_on_values_or_pointers
[3]Go 文档: https://golang.org/src/sync/mutex.go
很赞哦!(688)
相关文章
- 域名和网址一样吗?域名和网址有什么区别?
- 鸿蒙开发HUAWEI DevEco Device Tool 2.0 Beta1全新发布,提高开发效率
- 写了这么多年 JavaScript ,竟然还不知道这些技巧?
- 编程初学者采用何种方式学习编程会更有效率
- 一下域名,看有没有显示出你所解析的IP,如果有,就说明解析是生效的;如果没有,就说明解析是不生效的。
- 用Python自动化管理邮件简直太方便了,三个实用小例子带你体会!
- Web 现代应用程序架构下的性能优化,渐进式的极致艺术
- 微服务CI/CD实践-GitOps完整设计与实现
- 以上的就是为大家介绍的关于域名的详解域名注册:域名注册0
- 安全工程师必知:常见Java漏洞有哪些?
站长推荐
付款完成后,您只需耐心等待,如果您注册成功,系统会提示您。这里需要注意的是,域名是一个即时产品,只有在最终付款成功时才能预订,注册成功后不能更改。
良心推荐!Python爬虫高手必备的8大技巧!
七种交换变量值的方法,看看你知道几种
前端高效开发小技巧
为了避免将来给我们的个人站长带来的麻烦,在选择域名后缀时,我们的站长最好省略不稳定的后缀域名,比如n,因为我们不知道策略什么时候会改变,更不用说我们将来是否还能控制这个域名了。因此,如果站长不是企业,或者有选择的话,如果不能选择域名的cn类,最好不要选择它。
这12个关于软件测试的误解,是时候澄清了
Python自动化读取邮件基础代码讲解
两个经典例子让你彻底理解Java回调机制