美文网首页GolangGolang 开发者深入浅出golang
如何理解Golang的 “must not be copied

如何理解Golang的 “must not be copied

作者: HBLong | 来源:发表于2019-12-02 12:09 被阅读0次

    前言

    阅读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{}
    }
     
    ...
     
    

    相关文章

      网友评论

        本文标题:如何理解Golang的 “must not be copied

        本文链接:https://www.haomeiwen.com/subject/pbixgctx.html