Go Do not copy me

栏目: Go · 发布时间: 5年前

内容简介:当我们在写go程序时可能会看到类似的提示:在sync包的文档开始也进行了类似说明:或者在读详细文档时,经常看到出现频率很高的一句:

当我们在写 go 程序时可能会看到类似的提示:

call of xxx copies lock value: sync.WaitGroup contains sync.noCopy

在sync包的文档开始也进行了类似说明:

Values containing the types defined in this package should not be copied.

或者在读详细文档时,经常看到出现频率很高的一句:

must not be copied after first use

比如

A Mutex must not be copied after first use.

type Mutex struct {
        // contains filtered or unexported fields
}

The zero Map is empty and ready for use. A Map must not be copied after first use.

type Map struct {
        // contains filtered or unexported fields
}

A Cond must not be copied after first use.

type Cond struct {

        // L is held while observing or changing the condition
        L Locker
        // contains filtered or unexported fields
}

A Builder is used to efficiently build a string using Write methods. It minimizes memory copying. The zero value is ready to use. Do not copy a non-zero Builder.

type Builder struct {
        // contains filtered or unexported fields
}

爱问为什么的同学可能会问,为什么不让copy?

因为你copy一个Mutex的值没有任何意义,甚至会带来一些安全隐患。看下下面的代码:

type Temp struct {
    lock sync.Mutex
}

func (t *Temp) Lock() {
    t.lock.Lock()
}

func (t Temp) Unlock() {
    t.lock.Unlock()
}

func main() {
    t := Temp{lock: sync.Mutex{}}
    t.Lock()
    t.Unlock()
    t.Lock()
}

运行这段代码会出现死锁,原因就是Unlock方法是值作为接收者,unlock的Mutex是副本Mutex。

所以有的时候,我们可能不想让某个类型被拷贝,只想通过指针来使用,例如你的结构体有pointer字段,你不想让这个结构被拷贝,原因是拷贝后的指针字段指向同一个内容,这样会存在不安全的场景。

那如何防止拷贝某个类型呢?有下面两种方式:

  • 运行时检查
  • 使用go vet

运行时检查

比如strings.Builder

// A Builder is used to efficiently build a string using Write methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte
}
......
// noescape hides a pointer from escape analysis.  noescape is
// the identity function but escape analysis doesn't think the
// output depends on the input. noescape is inlined and currently
// compiles down to zero instructions.
// USE CAREFULLY!
// This was copied from the runtime; see issues 23382 and 7921.
//go:nosplit
func noescape(p unsafe.Pointer) unsafe.Pointer {
    x := uintptr(p)
    return unsafe.Pointer(x ^ 0)
}

func (b *Builder) copyCheck() {
    if b.addr == nil {
        // This hack works around a failing of Go's escape analysis
        // that was causing b to escape and be heap allocated.
        // See issue 23382.
        // TODO: once issue 7921 is fixed, this should be reverted to
        // just "b.addr = b".
        b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
    } else if b.addr != b {
        panic("strings: illegal use of non-zero Builder copied by value")
    }
}

// WriteString appends the contents of s to b's buffer.
// It returns the length of s and a nil error.
func (b *Builder) WriteString(s string) (int, error) {
    b.copyCheck()
    b.buf = append(b.buf, s...)
    return len(s), nil
}

// test.go
    var b strings.Builder
    for i := 3; i >= 1; i-- {
        fmt.Fprintf(&b, "%d...", i)
    }
    b.WriteString("ignition")
    fmt.Println(b.String())
    a := b
    a.WriteString("hello")
    fmt.Println(a.String())

strings.Builder的作用是最小化内存拷贝,这里边涉及到逃逸分析,关于逃逸分析可参考 这篇文章 ,通过内部的addr字段防止拷贝。如果拷贝之后调用WriteString,进入到copyCheck就会进入else分支,引发panic。sync.Cond也在运行时进行了检查:

type Cond struct {
    noCopy  noCopy
    L       Locker
    notify  notifyList
    checker copyChecker
}

// Signal wakes one goroutine waiting on c, if there is any.
//
// It is allowed but not required for the caller to hold c.L
// during the call.
func (c *Cond) Signal() {
    c.checker.check()
    runtime_notifyListNotifyOne(&c.notify)
}

// Broadcast wakes all goroutines waiting on c.
//
// It is allowed but not required for the caller to hold c.L
// during the call.
func (c *Cond) Broadcast() {
    c.checker.check()
    runtime_notifyListNotifyAll(&c.notify)
}

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")
    }
}

这里举个简单的例子:

type cond struct {
    checker copyChecker
}
type copyChecker uintptr
func (c *copyChecker) check() {
    fmt.Printf("Before: c: %v, *c: %v, uintptr(*c): %v, uintptr(unsafe.Pointer(c)): %v\n", c, *c, uintptr(*c), uintptr(unsafe.Pointer(c)))
    fmt.Println(atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))))
    fmt.Printf("After: c: %v, *c: %v, uintptr(*c): %v, uintptr(unsafe.Pointer(c)): %v\n", c, *c, uintptr(*c), uintptr(unsafe.Pointer(c)))
}

// main.go
var a cond
a.checker.check()
b := a
b.checker.check()

会输出如下信息:

Before: c: 0xc0000b0008, *c: 0, uintptr(*c): 0, uintptr(unsafe.Pointer(c)): 824634441736
true
After: c: 0xc0000b0008, *c: 824634441736, uintptr(*c): 824634441736, uintptr(unsafe.Pointer(c)): 824634441736
Before: c: 0xc0000b0018, *c: 824634441736, uintptr(*c): 824634441736, uintptr(unsafe.Pointer(c)): 824634441752
false
After: c: 0xc0000b0018, *c: 824634441736, uintptr(*c): 824634441736, uintptr(unsafe.Pointer(c)): 824634441752

在运行时检查都是使用了指针来进行测试是否发生了拷贝。

Go vet工具

vet是兽医的意思,而go的吉祥物是一支地鼠,而go vet工具报告可能出现的错误,所以这个命名还是蛮有意思的,猜测设计者应该是这个用意。

假如我们copy了sync.Cond,使用vet工具,在编译器就可以给开发者以提示,比如下面代码:

func main() {
    cc := sync.Cond{}
    copycc := cc
    fmt.Println(copycc)
}

使用go vet 工具来检查

go vet -copylocks  -json
{
        "goLandTest/escapeAna": {
                "copylocks": [
                        {
                                "posn": "/Users/hongyi/xxx/Go/src/goLandTest/escapeAna/main.go:69:12",
                                "message": "assignment copies lock value to copycc: sync.Cond contains sync.noCopy"
                        },
                        {
                                "posn": "/Users/hongyi/xxx/Go/src/goLandTest/escapeAna/main.go:70:14",
                                "message": "call of fmt.Println copies lock value: sync.Cond contains sync.noCopy"
                        }
                ]
        }
}

常见的IDE如vs code就集成了这个插件,当保存代码的时候会使用vet工具来检查代码。关于vet的更多用法请参考 官方文档

那如何让go vet来检查copy的呢?答案是在结构体内部嵌入noCopy,noCopy是一个结构体,在 cond.go 中可以找到

// noCopy may be embedded into structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
type noCopy struct{}

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

type Cond struct {
    noCopy noCopy
    ....
}
type WaitGroup struct {
    noCopy noCopy
    state1 [3]uint32
}

如果你想让你自己定义的类型不能被copy,你可以在你的包中简单的定义一个noCopy结构,并在你的类型里嵌入这个结构,然后go vet工具就会为你做剩下的检查工作。

type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type YourType struct {
   noCopy noCopy
   ...
}

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java Servlet & JSP Cookbook

Java Servlet & JSP Cookbook

Bruce W. Perry / O'Reilly Media / 2003-12-1 / USD 49.99

With literally hundreds of examples and thousands of lines of code, the Java Servlet and JSP Cookbook yields tips and techniques that any Java web developer who uses JavaServer Pages or servlets will ......一起来看看 《Java Servlet & JSP Cookbook》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具