Skip to content

Types & Data

The thesis of this module: pass by value, always

Section titled “The thesis of this module: pass by value, always”

Every single function call in Go copies its arguments. There is no exception, no hidden reference-passing mode, and no way to opt out. What makes this confusing at first is that some types are small handles that happen to point at shared data — copying the handle is still a copy, it just leaves the data it points to shared. Every surprise in this module (why a slice mutation is visible across a function call but an append is not, why a map write inside a function affects the caller, why a struct field write does not) is downstream of this single fact. Learn it here first, and Structs, Slices, Maps, and Pointers each become “here is what gets copied,” not four unrelated rules to memorize.

Every value in Go has a static type known at compile time. Go divides its types into two broad families:

Value types — variables hold the data directly. Assigning one variable to another copies the data. The primitive value types are booleans, numeric types (int, int64, float64, uint8, …), and string. Composite value types are arrays and structs.

Reference-like types — the variable holds a small handle that points to data allocated elsewhere: assigning always copies the handle itself, not the underlying data — that part is a plain, correct copy, just like copying any other value. Slices (a pointer + len + cap header), maps and channels (a single pointer), functions, and pointers (one word) all work this way, and because both copies of the handle still point at the same backing data, mutation through one is visible through the other. Strings are built the same way internally — a pointer + len header — but behave like a value type from the outside: string data is immutable, so sharing the header never produces the shared-mutation surprise that slices and maps can.

That immutability is exactly why []byte(s) has to copy: converting a string to a byte slice hands you something mutable, and the only way to guarantee the original string stays immutable is to give you your own backing array, not a view into the string’s.

s := "hello"
b := []byte(s)
b[0] = 'H'
fmt.Println(s, string(b)) // hello Hello — s never changed; b was an independent copy from the start

Verified on 1.26.5: exactly hello Hello. Converting back with string(b) copies again, for the same reason in reverse — once something is a string, nothing may ever mutate it through any other handle, so the runtime cannot just relabel b’s backing array as a string without copying it first.

flowchart TD
  Root["Go Type Taxonomy"]
  Root --> Value["Value types (copy on assign)"]
  Root --> Ref["Reference-like (header copied)"]
  Value --> V1["bool, int, float64, string"]
  Value --> V2["[N]T (array)"]
  Value --> V3["struct { ... }"]
  Ref --> R1["*T (pointer)"]
  Ref --> R2["[]T (slice)"]
  Ref --> R3["map[K]V"]
  Ref --> R4["chan T, func(...)"]
Go types split into value types (copied on assign) and reference-like types (header copied)
LessonTopic
This pageType landscape and a first program
StructsDefining structs, embedding, comparison
SlicesArrays vs slices, make, append, copy
Mapsmake(map[K]V), comma-ok, delete, iteration
Pointers& and *, mutation, nil pointers

The program below defines a Point struct, builds a []Point slice, then looks up a name in a map[string]int. It demonstrates value types (Point is copied), slice literals, and map access with the zero-value default.

package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
// Struct literal with named fields
p := Point{X: 3, Y: 4}
fmt.Println(p.X, p.Y)
// Slice of structs
points := []Point{
{X: 0, Y: 0},
{X: 1, Y: 2},
{X: 3, Y: 4},
}
for _, pt := range points {
fmt.Printf("(%d, %d)\n", pt.X, pt.Y)
}
// Map: string -> int
score := map[string]int{
"Alice": 95,
"Bob": 87,
}
fmt.Println(score["Alice"])
fmt.Println(score["Charlie"]) // missing key -> zero value (0)
}
What you getBenefitCost
value type (struct, array)predictable behavior, no aliasingcopy cost for large types
reference-like type (slice, map)shares data without copying itshared mutation — one goroutine’s edit can affect another
composite literalsreadable initializationsyntax differs per type
  • Arrays and slices are the same thing — an array’s size is part of its type, so [3]int ≠ [4]int; a slice is a dynamic view over an array.
  • A map is safe for concurrent access — a Go map is not — you need a sync.Mutex or sync.RWMutex if multiple goroutines read/write it at once.
Which of the following is a value type in Go — meaning assignment copies the data?
What does Go print when you read a map key that has never been set?
A []Point slice and a [3]Point array both store Point values. What is the key difference?