Clean code in Go · 3 MIN READ
Ownership: copy mutable data deliberately
Make it clear whether a caller lends, shares or transfers a buffer.
Make it clear whether a caller lends, shares or transfers a buffer. A slice is a view of a backing array, so assignment does not copy its elements. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.
Clean Go · A MAP OF THE IDEA
- Caller supplies mutable bytes
- Constructor copies retained state
- Accessor returns a separate copy
Useful when
- An object retains caller-owned mutable input
- Unexpected aliasing causes hard-to-find changes
Watch out
- Deep structures need more than a shallow copy
- Copies should be measured on hot paths
Implementation & operation cost: Low to medium
The design decision
A slice is a view of a backing array, so assignment does not copy its elements. An API that retains caller-owned bytes must define whether later mutation is allowed. Copying at an ownership boundary can be more valuable than making every method look immutable.
A complete Go example
Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.
package main
import (
"bytes"
"fmt"
)
type Message struct{ body []byte }
func NewMessage(input []byte) Message { return Message{body: bytes.Clone(input)} }
func (m Message) Bytes() []byte { return bytes.Clone(m.body) }
func main() {
input := []byte("go")
m := NewMessage(input)
input[0] = 'n'
output := m.Bytes()
output[0] = 'x'
fmt.Println(string(m.Bytes()))
}
// Output:
// go
The constructor copies input and Bytes copies output, so neither caller mutation changes the retained message. This makes the ownership contract concrete. It does not make arbitrary maps, nested pointers or concurrent methods safe automatically.
Tradeoffs and the simpler alternative
Copies cost memory and time, so measure large buffers before copying everywhere. A documented transfer or read-only borrowing contract may be preferable in a hot path. Mutexes protect shared access only when every relevant access follows the same lock discipline.
Describe ownership in the API contract
Returning a slice copies its descriptor, not its backing array. A caller can mutate shared elements even when append later allocates new storage. State whether the result is borrowed, immutable by convention or owned by the caller. Clone at the boundary when independent mutation is expected. A shallow clone of a slice of pointers still shares pointed-to values, so deep-copy behavior must match the actual domain structure.
Choose one concurrency model for mutable state
Either one goroutine owns the state and receives commands, a lock protects every relevant access, or callers receive isolated immutable snapshots. Mixing these models without a clear rule leaves races. A mutex guarding a map does not protect a slice stored in it after the method returns that slice. Return a snapshot under the lock, or keep later access inside the owner boundary. Test the aliasing case as well as running the race detector.
Make cleanup ownership visible
The code that successfully acquires a resource should define who closes it. Defer cleanup immediately after acquisition unless a documented ownership transfer occurs. A producer closes the channel it owns; arbitrary receivers should not close it to signal cancellation. Pass context for cancellation rather than sharing a mutable stop boolean. For a library API, document whether Close waits for goroutines and whether concurrent calls are supported.
TAKE THIS WITH YOU
Defines who may retain and mutate shared data. Copies cost memory and time, so measure large buffers before copying everywhere. A documented transfer or read-only borrowing contract may be preferable in a hot path. Mutexes protect shared access only when every relevant access follows the same lock discipline.
Check your understanding
Which statement best describes Ownership: copy mutable data deliberately?
Go to the source
Progress stays in this browser. No account needed.