如何理解Golang的 “must not be copied after first use”(源码解析)

前言

阅读Golang sync包时,总会看到一句话“must not be copied after first use”,对此感到很好奇,查阅过程中发现这篇文章总结得挺到位的,因此转载,记录一下,因为我只是对于原理上面好奇,因此没有全文翻译过来,只挑选了一些自己感兴趣的地方用自己的话总结了一下,感兴趣的可以看看原文章:
What does “nocopy after first use” mean in golang and how

正文

must not be copied after first use

初次使用后不能复制,sync包大多跟并发控制相关,出于安全考虑(避免指针的复制使得指针污染不安全,误操作而使程序崩溃)不能复制可以理解,但Golang是怎么样办到的呢,接下来就从源码层面看看

1. 运行时检测,实例地址值传递

这个是在初次时候后记录变量地址,二次使用时比对变量地址,如果不同的话说明被复制了。
首先,我们先来看一个比较明显的例子strings.Builder

type Builder struct {
    addr *Builder     // 关键所在,专门用来记录Builder实例的地址
    buf []byte
}
func (b *Builder) copyCheck() {
    if b.addr == nil {
        // 初始化,记录b实例的地址
        b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
    } else if b.addr != b {
        panic("strings: illegal use of non-zero Builder copied by value")
    }
}
func (b *Builder) Write(p []byte) (int, error) {
    b.copyCheck()
    ...
}
    
// test case
var a strings.Builder
a.Write([]byte("testa"))
var b = a
b.Write([]byte("testb"))   // 这里是复制后使用,所以会诱发panic

很明显,strings.Builder通过一个指针来存储实例化后的实例地址,由于这个值是由内部赋值的,所以初次使用时为 nil,此时会存储地址,下次使用的时候会进行比对,不一致,说明被复制过了

接下来,我们回到sync包,来看看sync.Cond怎么处理

type Cond struct {
    noCopy  noCopy
    L       Locker
    notify  notifyList
    checker copyChecker
}
type copyChecker uintptr
func (c *copyChecker) check() {
    if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
       !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
       uintptr(*c) != uintptr(unsafe.Pointer(c)) {
           panic("sync.Cond is copied")
    }
}
func (c *Cond) Wait() {
    c.checker.check()
    ...
}

这里跟strings.Builder有点不一样,因为sync.Cond通过一个结构体copyChecker来进行判断处理,咱们来看看关键代码check()

if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
       !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
       uintptr(*c) != uintptr(unsafe.Pointer(c)) {
           panic("sync.Cond is copied")
    }

我们假设一下创建了一个cond,cond := sync.NewCond(new(sync.Mutex)),此时假设内存如 "cond内存示例假设图" 第一部分所示。接下来再调用cond.Wait()后会触发check()里面的

!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c)))

由于此时checker值为0,因此会把checker的地址存储进去,那么此时cond.checker的值为cond.checker的地址(假设是0x04)

接下来当复制该变量condB := cond时,整块空间会被复制到一个新内存(假设此时checker地址为0x0A),这个时候如果再次调用cond.Wait(),那么一比对就会发现cond被复制了,于是乎就起到了复制检测的功能

cond内存示例假设图

2. 静态代码检测,通过go vet

-copylocks是go vet的一个flag,用来开启是否有不允许拷贝但被拷贝的代码检测,只需要定义一个结构体noCopy,然后嵌入到你不允许拷贝的结构体。如果你希望自己定义的一个结构体使用者无法拷贝,只能指针传递保证全局唯一的话,也可以使用这个方法处理

// noCopy may be embedded into structs which must not be copied
// after the first use.
type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) UnLock() {}

实例代码:

// file: test.go
package main
    
type noCopy struct{}
    
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}
    
// sync.Pool
type Pool struct {
    noCopy noCopy
    val    int
}
    
func main() {
    poolA := Pool{}
    poolB := poolA
    poolB.val = 1024
}

然后通过命令go vet -copylocks ./test.go就可以检测到错误

$ go vet -copylocks ./test.go
# command-line-arguments
.\test.go:16:11: assignment copies lock value to poolB: command-line-arguments.Pool contains command-line-arguments.noCopy

回到sync包,我们也能看到一样的身影

type Cond struct {
    noCopy noCopy
    
    // L is held while observing or changing the condition
    L Locker
  
    notify  notifyList
    checker copyChecker
}

type Pool struct {
    noCopy noCopy
 
    local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
    localSize uintptr        // size of the local array
 
    victim     unsafe.Pointer // local from previous cycle
    victimSize uintptr        // size of victims array
 
    // New optionally specifies a function to generate
    // a value when Get would otherwise return nil.
    // It may not be changed concurrently with calls to Get.
    New func() interface{}
}
 
...
 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342