Detect locks passed by value in Go

Michał Łowicki
golangspec
3 min readNov 17, 2016

--

Introduction to `go tool vet -copylocks`

Go is shipped with vet command line tool. It runs set of heuristics on a source code to find suspicious constructs like unreachable code or calls to fmt.Printf where arguments aren’t aligned with desired format:

package mainimport "fmt"func f() {
fmt.Printf("%d\n")
return
fmt.Println("Done")
}
> go tool vet vet.go
vet.go:8: unreachable code
vet.go:6: missing argument for Printf("%d"): format reads arg 1, have only 0 args

This story is about one option specifically — copylocks. Let’s what it does and how it can be useful in real-world programs.

Suppose the program uses mutex for synchronization:

package mainimport "sync"type T struct {
lock sync.Mutex
}
func (t *T) Lock() {
t.lock.Lock()
}
func (t T) Unlock() {
t.lock.Unlock()
}
func main() {
t := T{lock: sync.Mutex{}}
t.Lock()
t.Unlock()
t.Lock()
}

If v is addressable and &v’s method set contains m, v.m() is shorthand for (&v).m()

Think for a moment what might be the result of running what is implemented above…

Program falls into a deadlock:

fatal error: all goroutines are asleep — deadlock!goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x4201162ac)
/usr/local/go/src/runtime/sema.go:47 +0x30
sync.(*Mutex).Lock(0x4201162a8)
/usr/local/go/src/sync/mutex.go:85 +0xd0
main.(*T).Lock(0x4201162a8)
...

It’s not good and the root cause is passing receiver by value to Unlock method so t.lock.Unlock() is actually called on a copy of the lock. It’s very easy to overlook, especially in bigger programs. It isn’t detected by the compiler since this might be an intention of the programmer. This is where vet steps in…

> go tool vet vet.go
vet.go:13: Unlock passes lock by value: main.T

Option copylocks (enabled by default) checks if passed by value is something of a type having Lock method with pointer receiver. If this is the case then it throws a warning.

Example use of this mechanism is in the sync package itself. There is a special type named noCopy. To protect a type from copying by value (actually make it detectable by the vet tool), single field needs to be added to a struct like for WaitGroup:

package mainimport "sync"type T struct {
wg sync.WaitGroup
}
func fun(T) {}func main() {
t := T{sync.WaitGroup{}}
fun(t)
}
> go tool vet lab.go
lab.go:9: fun passes lock by value: main.T contains sync.WaitGroup contains sync.noCopy
lab.go:13: function call copies lock value: main.T contains sync.WaitGroup contains sync.noCopy

Under the hood

Sources are placed in /src/cmd/vet. Every option for vet registers itself using register function which takes (among others) a variadic parameter of types of AST nodes that option is interested in and a callback. That callback function will be fired for every node of specified types. For copylocks nodes to investigate are i.e. return statements. Ultimately it all goes to lockPath which verifies if passed value is of type which has a pointer receiver method named Lock. During the whole process go/ast package is used extensively. A gentle introduction to that package can be found in Go’s Testable Examples under the hood.

👏👏👏 below to help others discover this story. Please follow me here or on Twitter if you want to get updates about new posts or boost work on future stories.

Resources

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.