noCopy Strategies You Should Know in Golang

Preventing Copy Catastrophes: NoCopy in Go

Nothing ever goes away until it has taught us what we need to know.
— Pema Chödrön

Medium Link: noCopy Strategies You Should Know in Golang | by Wesley Wei | Sep, 2024 | Programmer’s Career
Author:Wesley Wei – Medium

Hello, here is Wesley, Today’s article is about noCopy in Go. Without further ado, let’s get started.💪

1.1 Sync.noCopy

When learning about WaitGroup code in Go, I noticed the noCopy and saw a familiar comment: “must not be copied after first use”.

1
2
3
4
5
6
7
8
9
10
// A WaitGroup must not be copied after first use.
//
// In the terminology of the Go memory model, a call to Done
// “synchronizes before” the return of any Wait call that it unblocks.
type WaitGroup struct {
noCopy noCopy

state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
sema uint32
}

Through searching, I found that “must not be copied after first use” and the noCopy often appear together.

image.png

1
2
3
4
5
6
// Note that it must not be embedded, due to the Lock and Unlock methods.
type noCopy struct{}

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

By observing the definition of noCopy Definition in Go 1.23, I found:

  1. The noCopy type is an empty struct.
  2. The noCopy type implements two methods: Lock and Unlock, which are both no-op methods.
  3. The comment emphasizes that Lock and Unlock are used by the go vet checker.

The noCopy type has no practical functional properties, and I think it’s only through thinking and experimenting that one can understand its specific purpose, and why “must not be copied after first use”?

1.2 Go Vet and “Locks Erroneously Passed by Value”

When we input the following command:

1
go tool vet help copylocks

output:

1
2
3
4
5
copylocks: check for locks erroneously passed by value

Inadvertently copying a value containing a lock, such as sync.Mutex or
sync.WaitGroup, may cause both copies to malfunction. Generally such
values should be referred to through a pointer.

We get a message from Go Vet, which tells us that when using values containing locks (such as sync.Mutex or sync.WaitGroup) and passing them by value), it may cause unexpected problems. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"sync"
)

type T struct {
lock sync.Mutex
}

func (t T) Lock() {
t.lock.Lock()
}

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

func main() {
var t T
t.Lock()
fmt.Println("test")
t.Unlock()
fmt.Println("finished")
}

If we run this code, it will output an error message:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// output
test
fatal error: sync: unlock of unlocked mutex

goroutine 1 [running]:
sync.fatal({0x4b2c9b?, 0x4a14a0?})
/usr/local/go-faketime/src/runtime/panic.go:1031 +0x18

// ❯ go vet .

# noCopy

./main.go:12:9: Lock passes lock by value: noCopy.T contains sync.Mutex
./main.go:15:9: Unlock passes lock by value: noCopy.T contains sync.Mutex

The reason for this error is that the Lock and Unlock methods use a value receiver t, which creates a copy of T when calling the method. This means that the lock instance in Unlock does not match the one in Lock .

To fix this, we can change the receiver to a pointer type:

1
2
3
4
5
6
7
func (t *T) Lock() {
t.lock.Lock()
}

func (t *T) Unlock() {
t.lock.Unlock()
}

Similarly, when using Cond, WaitGroup and other types that contain locks, we need to ensure that they are not copied after the first use. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"sync"
"time"
)

func worker(id int, wg sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}

func main() {
var wg sync.WaitGroup

for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, wg) // passes lock by value
}

wg.Wait()

fmt.Println("All workers done!")
}

If we run this code, it will also output an error message:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/////
Worker 3 starting
Worker 1 starting
Worker 2 starting
Worker 1 done
Worker 3 done
Worker 2 done
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000108040?)

// ❯ go vet .

# noCopy

./main.go:9:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
./main.go:21:16: call of worker copies lock value: sync.WaitGroup contains sync.noCopy

To fix this, we can use the same wg instance, I encourage you to try it. More information about copylocks: copylock.

1.3 Trying out go vet detection

The design of go vet‘s noCopy mechanism is a way to prevent structures from being copied, especially for those that contain synchronization primitives (such as sync.Mutex and sync.WaitGroup). The purpose is to prevent unexpected copylocks from occurring, but this prevention is not mandatory; whether or not to copy needs to be detected by the developer. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

type noCopy struct{}

func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}

type noCopyData struct {
Val int32
noCopy
}

func main() {
c1 := noCopyData{Val: 10}
c2 := c1
c2.Val = 20
fmt.Println(c1, c2)
}

The above example doesn’t have any practical use; the program can run correctly but go vet will prompt a “passes lock by value” warning. This is just an exercise to try out the detection mechanism of go vet.

However, if you need to write code related to synchronization primitives (such as sync.Mutex and sync.WaitGroup), the noCopy mechanism may be useful for you.

1.4 Other noCopy Strategies

From what we’ve learned, go vet can detect potential copy issues that are not strictly prohibited. Are there any strategies that strictly prohibit copying? Yes, there are. Let’s take a look at the source code for strings.Builder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// A Builder is used to efficiently build a string using [Builder.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

// External users should never get direct access to this buffer,
// since the slice at some point will be converted to a string using unsafe,
// also data between len(buf) and cap(buf) might be uninitialized.
buf []byte
}

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)(abi.NoEscape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}

// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
b.copyCheck()
b.buf = append(b.buf, p…)
return len(p), nil
}

The key point is:

1
b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b)))

This line of code does the following:

  1. unsafe.Pointer(b): converts b to an unsafe.Pointer, so that it can be used with abi.NoEscape.
  2. abi.NoEscape(unsafe.Pointer(b)): tells the compiler that b will not escape, i.e., it can continue to be allocated on the stack instead of the heap.
  3. (*Builder)(…): converts the abi.NoEscape return value back to a *Builder type, so that it can be used normally.
  4. Finally, b.addr is set to the address of b itself, which prevents Builder from being copied (with a check for b.addr != b in the following logic).

go1.23.0 builder.go
abi.NoEscape

Using strings.Builder with copying behavior will cause a panic:

1
2
3
4
5
6
7
8
9
10
func main() {
var a strings.Builder
a.Write([]byte("a"))
b := a
b.Write([]byte("b"))
}
// output
panic: strings: illegal use of non-zero Builder copied by value
goroutine 1 [running]:
strings.(*Builder).copyCheck(…)

Another example is sync.Cond, which can be referred to in the article “What does “nocopy after first use” mean in golang and how | by Jing | Medium“.

1.5 Summary

  1. Synchronization primitives (such as sync.Mutex and sync.WaitGroup) should not be copied, because once they are copied, their internal state will be duplicated, leading to concurrency issues.
  2. Although Go itself does not provide a mechanism to strictly prevent copying, the noCopy struct provides a non-strict mechanism for identifying and detecting copies using the go vet tool.
  3. Some source code in Go performs no-copy checks at runtime and returns a panic, such as strings.Builder and sync.Cond.

References

Detect locks passed by value in Go | by Michał Łowicki | golangspec | Medium
What does “nocopy after first use” mean in golang and how | by Jing | Medium


More Series Articles about You Should Know In Golang:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

And I’m Wesley, delighted to share knowledge from the world of programming. 

Don’t forget to follow me for more informative content, or feel free to share this with others who may also find it beneficial. it would be a great help to me.

Give me some free applauds, highlights, or replies, and I’ll pay attention to those reactions, which will determine whether or not I continue to post this type of article.

See you in the next article. 👋

中文文章: https://programmerscareer.com/zh-cn/golang-nocopy/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: Originally written at https://programmerscareer.com/golang-nocopy/ at 2024-09-07 00:19. If you choose to repost or use this article, please cite the original source.

[Pinned🔝] Sticky Note Collections in Go

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×