When we read golang source code or learn to use some builtin structs, frequently we may be told that “must not be copied after first use”, such as sync.Cond, sync.Map, sync.Mutex (nearly all types in sync package) and strings.Builder. Most of the time it is required so for safety reasons, for example you have a struct with a pointer field and you don’t want it to be copied since a shallow copy will make these two hold the same pointer and be unsafe. So what does golang do to ensure that? There isn’t a perfect answer as far as I know (see discussions here), but in this story, I will review the following two solutions and hope it provide a better understanding.
1. runtime checking
This is done by encapsulating a pointer to itself and check before any further operation. A typical example is strings.Builder
:
type Builder struct {
addr *Builder
buf []byte
}func (b *Builder) copyCheck() {
if b.addr == nil {
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")) // will panic here
As you can see, when we declare builder a
and write, a.addr
will be assigned its own address. After we assign a
to b
, a.addr
will be shallowly copied to b.addr
, but the newly allocated address of b
is certainly not the same as a.addr
, so panic happens. This solution utilizes the fact that pointer is shallowly copied and easy to understand.
Another example is sync.Cond
:
type Cond struct {
noCopy noCopy
L Locker
notify notifyList
checker copyChecker
}type copyChecker uintptrfunc (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()
...
}
It is a bit difficult to read the check()
function at first sight, so let’s define a similar struct and try to find out why:
type cond struct {
checker copyChecker
}type copyChecker uintptrfunc (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)))
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)))
}// test case
var a cond
a.checker.check()
b := a
b.checker.check()// results
Before: c: 0x414020, *c: 0, uintptr(*c): 0, uintptr(unsafe.Pointer(c)): 4276256
After: c: 0x414020, *c: 4276256, uintptr(*c): 4276256, uintptr(unsafe.Pointer(c)): 4276256
Before: c: 0x414044, *c: 4276256, uintptr(*c): 4276256, uintptr(unsafe.Pointer(c)): 4276292
After: c: 0x414044, *c: 4276256, uintptr(*c): 4276256, uintptr(unsafe.Pointer(c)): 4276292
Clearly when we declare a
, its checker
field is 0
and the address of checker field is 0x414020
or the decimal 4276256
. After CompareAndSwapUintptr()
, its checker field is assigned its own address, say 4276256
. When we assign a
to b
, the checker
field of a
is copied to that of b
, but the address of b’s checker
field is actually 0x414044
or the decimal 4276292
. So it finally meets all three conditions(in fact two) and detects copy. This is still a “self pointer” approach and I guess you got it.
To summarize, runtime checking often uses a self pointer and won’t check until runtime.
2. Go vet copylocks checking
-copylocks
is actually a go vet
flag which checks whether a locker
type is copied or not. A locker
type is a type which has Lock()
and Unlock()
methods. See here for more details. As mentioned before, nearly all types in sync
package must not be copied and actually it’s guaranteed just by encapsulating a noCopy
struct:
// src/sync/cond.go
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}// sync.Pool
type Pool struct {
noCopy noCopy
...
}// sync.WaitGroup
type WaitGroup struct {
noCopy noCopy
...
}
Go vet
will examine every statement and operand for a locker
type and in this way copy could be found before runtime. So if you want a type to be not copied, what you need to do is simply defining a noCopy
struct in your package and encapsulating it as extra field like the following:
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}type MyType struct {
noCopy noCopy
...
}
Then go vet
will do the check work for you. That’s it.
Conclusion
Though currently we don’t have a perfect answer for this issue, these two approaches are still instructive in similar situations. Self pointer or go vet check? Give it a try next time :)