1) Arrays
Basics
An array in Go is a fixed-size sequence of elements of the same type. Its length is part of its type: [4]int
and [5]int
are distinct types.
- Arrays are value types: assignment or function call copies all elements.
- Zero value is an array of zero-valued elements.
- Useful for fixed-size data (e.g.
[32]byte
crypto keys). - Arrays can be compared if element type is comparable.
package main
import "fmt"
func main() {
var a [3]int // [0 0 0]
b := [3]int{1, 2, 3} // literal
c := [...]string{"x", "y", "z"} // length inferred
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c, "len:", len(c))
}
Copy Semantics
Assigning arrays duplicates all contents. To avoid copying, pass a pointer to an array.
package main
import "fmt"
func mutate(arr [3]int) { arr[0] = 99 }
func mutatePtr(arr *[3]int) { arr[0] = 99 }
func main() {
a := [3]int{1, 2, 3}
mutate(a) // works on a copy
fmt.Println("after mutate:", a) // [1 2 3], unchanged
mutatePtr(&a) // modify original
fmt.Println("after mutatePtr:", a) // [99 2 3]
}
2) Slices
Slicing Syntax a[i:j:k]
Slicing makes a new view into the same array. There are two forms:
a[i:j]
(2-index): works on arrays, slices, pointers to arrays, and strings.a[i:j:k]
(3-index, full slice): works on slices, pointers to arrays (*[N]T
), and array variables. Not allowed on array literals.- The third index
k
controls the capacity of the result slice and stops it from growing past that limit.
Key rule
a[i:j]
=> elements with indicesi
up to j-1 (length =j - i
).- Defaults: omitted
i
=>0
; omittedj
=>len(a)
; soa[:]
is the whole slice. - Bounds for
a[i:j]
:0 ≤ i ≤ j ≤ len(a)
. a[i:j:k]
(full slice) also sets capacity: result cap =k - i
.- Bounds for full slice:
0 ≤ i ≤ j ≤ k ≤ cap(a)
. - Full slice requires a slice or a pointer to an array (
*[N]T
). It’s not allowed on plain strings and not on a non-addressable array value.
package main
import "fmt"
func main() {
a := []int{0, 1, 2, 3, 4, 5}
// Basic 2-index slicing:
s1 := a[1:4] // [1 2 3], len=3, cap from a's backing starting at index 1
fmt.Println("s1:", s1, "len:", len(s1), "cap:", cap(s1))
// Omitting bounds:
s2 := a[:3] // [0 1 2]
s3 := a[3:] // [3 4 5]
s4 := a[:] // whole slice
fmt.Println("s2:", s2, "s3:", s3, "s4:", s4)
// Full slice (3-index): control capacity of the result.
// Here we cap the view to stop it from growing into the rest.
s5 := a[1:4:4] // [1 2 3], len=3, cap=3 (since k=4 and i=1 => 4-1=3)
fmt.Println("s5:", s5, "len:", len(s5), "cap:", cap(s5))
// With full slice, appending past len forces a new backing array (since cap==len):
s5 = append(s5, 99)
fmt.Println("s5 after append:", s5, "cap:", cap(s5))
// Slicing arrays produces a slice:
arr := [...]int{10, 11, 12, 13}
s6 := arr[1:3] // OK: 2-index on array yields slice [11 12]
// Full slice on array pointer (not plain array):
p := &arr
s7 := p[1:3:3] // OK: full slice on *[N]T => [11 12], cap=2
fmt.Println("s6:", s6, "s7:", s7, "cap(s7):", cap(s7))
}
// s1: [1 2 3] len: 3 cap: 5
// s2: [0 1 2] s3: [3 4 5] s4: [0 1 2 3 4 5]
// s5: [1 2 3] len: 3 cap: 3
// s5 after append: [1 2 3 99] cap: 6
// s6: [11 12] s7: [11 12] cap(s7): 2
Indexing intuition
i
is inclusive start; j
is exclusive end. The third index k
limits capacity, which is useful to avoid accidental growth into adjacent elements of the same backing array.Slice Header
A slice is a descriptor of an underlying array: pointer + length + capacity.
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
a := [...]int{0, 1, 2, 3, 4, 5}
s := a[1:4] // [1 2 3]
fmt.Println("s:", s, "len:", len(s), "cap:", cap(s))
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("header: Data=%#x Len=%d Cap=%d\n", hdr.Data, hdr.Len, hdr.Cap)
s[0] = 99
fmt.Println("after s[0]=99, a:", a)
}
// s: [1 2 3] len: 3 cap: 5
// header: Data=0xc000104008 Len=3 Cap=5
// after s[0]=99, a: [0 99 2 3 4 5]
Alias semantics
Make and Append
make([]T, len, cap)
allocates a backing array. Appending grows the slice, possibly allocating a new array if capacity is exceeded.
package main
import "fmt"
func main() {
s := make([]int, 0, 2)
fmt.Println("start:", s, "len:", len(s), "cap:", cap(s))
s = append(s, 1, 2)
fmt.Println("after append 1,2:", s, "len:", len(s), "cap:", cap(s))
// Next append exceeds capacity => runtime allocates a new backing array
s = append(s, 3)
fmt.Println("after append 3:", s, "len:", len(s), "cap:", cap(s))
// Show that appending may detach from old backing array
t := s[:2]
s = append(s, 4, 5, 6, 7)
fmt.Println("t (aliased prefix):", t)
fmt.Println("s (after growth):", s, "cap:", cap(s))
}
// start: [] len: 0 cap: 2
// after append 1,2: [1 2] len: 2 cap: 2
// after append 3: [1 2 3] len: 3 cap: 4
// t (aliased prefix): [1 2]
// s (after growth): [1 2 3 4 5 6 7] cap: 8
Growth strategy
Pitfalls
- Holding a small slice of a large array keeps the entire array in memory.
append
may reallocate: old references won’t see new elements.- Use the 3-index slice
a[i:j:k]
to limit capacity and prevent leaks.
package main
import "fmt"
func main() {
big := make([]byte, 0, 1_000_000) // huge backing array capacity
// Pretend we only need first 100 bytes of it:
small := big[:100]
fmt.Println("before trim len/cap:", len(small), cap(small))
// Trim capacity to length so the rest of the big array isn't retained by small.
small = small[:len(small):len(small)]
fmt.Println("after trim len/cap:", len(small), cap(small))
// Demonstrate append creates a new backing array (since cap == len).
small = append(small, 1)
fmt.Println("after append len/cap:", len(small), cap(small))
}
// before trim len/cap: 100 1000000
// after trim len/cap: 100 100
// after append len/cap: 101 208
3) Maps
Basics
A map is a hash table: map[K]V
. Keys must be comparable (no slices, maps, functions).
- Zero value is
nil
. Lookups on nil map are safe; writes panic. - Iteration order is deliberately randomized.
len(m)
returns number of entries.
package main
import "fmt"
func main() {
// Safe lookup on nil map:
var nilMap map[string]int
fmt.Println("lookup on nil map:", nilMap["x"]) // 0
// But writes to nil map panic; use make:
m := make(map[string]int)
m["a"] = 1
m["b"] = 2
if v, ok := m["a"]; ok {
fmt.Println("m[\"a\"] =", v)
}
fmt.Println("len:", len(m))
for k, v := range m {
fmt.Printf("%s=%d ", k, v)
}
fmt.Println()
}
// lookup on nil map: 0
// m["a"] = 1
// len: 2
// a=1 b=2
Implementation
Maps use an array of buckets. Each bucket holds up to 8 key/value pairs. Overflow buckets are chained as needed.
Load factor
Best Practices
- Preallocate with
make(map[K]V, n)
to avoid rehashing. - Use the
ok
idiom for safe lookups. - Do not rely on iteration order.
- Not safe for concurrent writes – use sync.Map or locks.
4) Sets via Maps
Idiomatic Set
Go has no built-in set type. The idiom: map[T]struct{}
.
package main
import "fmt"
type IntSet map[int]struct{}
func (s IntSet) Add(x int) { s[x] = struct{}{} }
func (s IntSet) Remove(x int) { delete(s, x) }
func (s IntSet) Has(x int) bool {
_, ok := s[x]
return ok
}
func main() {
s := make(IntSet)
s.Add(2)
s.Add(5)
fmt.Println("Has 2?", s.Has(2))
fmt.Println("Has 3?", s.Has(3))
s.Remove(2)
fmt.Println("Has 2 now?", s.Has(2))
}
Why struct{}?
struct{}
takes 0 bytes. Using map[T]bool
wastes space.5) Performance & Best Practices
Preallocation
Use make
with capacity hints to reduce allocations.
package main
import "fmt"
func main() {
s := make([]int, 0, 1000) // ready for ~1000 appends
for i := 0; i < 5; i++ {
s = append(s, i)
fmt.Println("len:", len(s), "cap:", cap(s))
}
m := make(map[string]int, 4) // initial space for ~4 entries
m["a"], m["b"], m["c"], m["d"] = 1, 2, 3, 4
fmt.Println("map len:", len(m))
}
Copy vs Loop
Prefer the built-in copy
function over manual loops. It’s optimized in assembly.
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
n := copy(dst, src)
fmt.Println("copied:", n, "dst:", dst)
}
Memory Safety
- Trim slice capacity to allow GC of unused backing arrays.
- Avoid keeping references to huge arrays through small slices.
- For maps: use
nil
only if read-only (writes panic). For slices:nil
vs[]T{}
are both valid; choose based on semantics (e.g., JSONnull
vs[]
).