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.
The Go type landscape
Section titled “The Go type landscape”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 startVerified 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(...)"] Module map
Section titled “Module map”| Lesson | Topic |
|---|---|
| This page | Type landscape and a first program |
| Structs | Defining structs, embedding, comparison |
| Slices | Arrays vs slices, make, append, copy |
| Maps | make(map[K]V), comma-ok, delete, iteration |
| Pointers | & and *, mutation, nil pointers |
Your first composite-type program
Section titled “Your first composite-type program”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)} Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| value type (struct, array) | predictable behavior, no aliasing | copy cost for large types |
| reference-like type (slice, map) | shares data without copying it | shared mutation — one goroutine’s edit can affect another |
| composite literals | readable initialization | syntax differs per type |
Common Misconceptions
Section titled “Common Misconceptions”- 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.Mutexorsync.RWMutexif multiple goroutines read/write it at once.