The idea in one minute#
In Go, everything is a value, and assignment copies the value. Passing an argument copies it. Returning it copies it. There are no hidden references.
Some values contain a pointer — slices, maps, channels, strings, interfaces — so copying them copies a small header that still points at the same underlying data. That single rule, “the copy is always shallow and always of the value itself”, explains every sharing surprise in Go.
Every type also has a zero value — 0, "", false, nil — and every variable starts
there. There is no uninitialized memory.
An analogy#
Photocopying a page. If the page holds the recipe, you now have two independent recipes. If the page holds only “the recipe is in the blue folder on shelf 3”, you have two notes pointing at one folder — change the folder and both readers see it.
A picture#
flowchart LR
subgraph COPY["b := a"]
direction TB
A1["a = [3]int{1,2,3}<br/>24 bytes of data"] -->|"copies all 24 bytes"| B1["b = [3]int{1,2,3}<br/>independent"]
S1["s = []int{1,2,3}<br/>header: ptr, len, cap"] -->|"copies the 24-byte header"| S2["t = header copy"]
S1 --> BK[("backing array<br/>1, 2, 3")]
S2 --> BK
end
class A1,B1 compute
class S1,S2 queue
class BK memoryHow it really works#
The built-in types#
| Kind | Types | Size (64-bit) | Zero value |
|---|---|---|---|
| Boolean | bool | 1 byte | false |
| Integers | int8/16/32/64, uint8/16/32/64 | as named | 0 |
int, uint, uintptr | 8 bytes (machine word) | 0 | |
| Aliases | byte = uint8, rune = int32 | 1, 4 | 0 |
| Floats | float32, float64 | 4, 8 | 0 |
| Complex | complex64, complex128 | 8, 16 | 0 |
| String | string | 16-byte header (pointer, length) | "" |
| Pointer | *T | 8 | nil |
| Array | [N]T | N × size of T | every element zero |
| Slice | []T | 24-byte header (pointer, length, capacity) | nil |
| Map, channel, function | map[K]V, chan T, func(...) | 8 (a pointer) | nil |
| Interface | any, error, … | 16 (two words) | nil |
| Struct | struct{...} | its fields plus padding | every field zero |
Module II opens up each of the composite ones.
Declaring#
var n int // zero value: 0
var s string = "go" // explicit type
x := 42 // short form, inside functions only; type inferred (int)
var a, b = 1, "two" // several at once
const Pi = 3.14159 // constant; untyped until usedUntyped constants have arbitrary precision and take a type from context: const big = 1 << 40
is fine, and var f float32 = Pi works without a conversion.
Go never converts between types implicitly. int32 plus int64 is a compile error; write
int64(a) + b. This is deliberate: every widening, narrowing and sign change is visible.
Copies: the rule and its consequences#
| Type | Assignment copies | Afterwards the two values |
|---|---|---|
Numbers, bool | The value | Are independent |
Array [N]T | All N elements | Are independent |
| Struct | Every field | Are independent — unless a field is one of the rows below |
Pointer *T | The address | Point at the same variable |
| Slice | The header (pointer, len, cap) | Share the backing array |
| Map, channel | The pointer | Are the same map or channel |
| String | The header | Share bytes, which is safe: strings are immutable |
| Interface | Two words (type, data) | Usually share what the data word points to |
This is also why you pass a pointer when a function must modify its argument: the function receives a copy, and a copy of a pointer still reaches the original.
Zero values you can use#
Go’s standard library is designed so that zero values are useful: a sync.Mutex is unlocked, a
bytes.Buffer is empty and ready, a nil slice can be appended to, len of a nil map is 0. The
one trap: writing to a nil map panics — maps must be made with make or a literal.
Defining your own types#
type Celsius float64 // a new type with float64's representation
type Tokens []int // a named slice type
type ID = string // an alias: ID *is* stringA defined type is distinct from its underlying type — Celsius and float64 do not mix without
a conversion — which lets the compiler catch unit mistakes for free.
Code#
// values.go — sizes, zero values, and what an assignment actually copies.
package main
import (
"fmt"
"unsafe"
)
type Point struct{ X, Y float64 }
func main() {
fmt.Println("sizes in bytes")
fmt.Println(" bool ", unsafe.Sizeof(true))
fmt.Println(" int ", unsafe.Sizeof(int(0)))
fmt.Println(" float32 ", unsafe.Sizeof(float32(0)))
fmt.Println(" string ", unsafe.Sizeof(""), "(header only, whatever the length)")
fmt.Println(" []int ", unsafe.Sizeof([]int{}), "(header only)")
fmt.Println(" [3]int ", unsafe.Sizeof([3]int{}), "(the data itself)")
fmt.Println(" map ", unsafe.Sizeof(map[string]int{}), "(a pointer)")
fmt.Println(" any ", unsafe.Sizeof(any(nil)), "(two words)")
fmt.Println(" Point ", unsafe.Sizeof(Point{}))
var (
i int
s string
p *Point
m map[string]int
e error
)
fmt.Printf("\nzero values: %d %q %v %v %v (len of nil map: %d)\n", i, s, p, m, e, len(m))
// Array: the copy is independent.
a := [3]int{1, 2, 3}
b := a
b[0] = 99
fmt.Println("\narray a:", a, " b:", b, " ← independent")
// Slice: the header is copied, the backing array is shared.
sl := []int{1, 2, 3}
tl := sl
tl[0] = 99
fmt.Println("slice s:", sl, " t:", tl, " ← shared")
// Struct: copied field by field.
p1 := Point{1, 2}
p2 := p1
p2.X = 99
fmt.Println("struct p1:", p1, " p2:", p2, " ← independent")
// Pointer: the address is copied.
q := &p1
q.X = -1
fmt.Println("via pointer, p1 is now", p1)
// A function receives a copy.
double := func(pt Point) { pt.X *= 2 }
doubleP := func(pt *Point) { pt.X *= 2 }
double(p1)
fmt.Println("after double(p1): ", p1, " ← unchanged")
doubleP(&p1)
fmt.Println("after doubleP(&p1):", p1, " ← changed")
}Remember this#
- Assignment, argument passing and return all copy the value.
- Slices, maps, channels, strings and interfaces are small headers that point elsewhere: the header is copied, the data is shared.
- Every variable starts at its type’s zero value; design types so the zero value is usable.
- No implicit conversions between numeric types.
Try it#
- Run
values.go. Add a struct with a slice field, copy it, and change an element through the copy. Which parts were independent? - Declare
var m map[string]intand write to it. Read the panic message, then fix it two ways. - Define
type Meters float64andtype Feet float64. What does the compiler say when you add one to the other?
Check yourself#
- What does assigning one slice to another copy?
- Why does Go have no implicit numeric conversions?
- Which zero value cannot be written to?