Data structures
Go provides a small set of built-in composite types: arrays (fixed-size sequences), slices (growable views into arrays — the conventional “list” type), maps (hash tables), structs (records of named fields), and channels (treated separately in Concurrency). The standard library does not provide an extensive collection framework — the built-ins cover most needs, and the container/list, container/heap, container/ring, and sort packages provide additional structures. The conventional Go discipline favours slices and maps over more elaborate types; the simplicity admits substantial transparency at the cost of some functional richness.
Arrays
Fixed-size; the length is part of the type:
var arr [5]int // [0 0 0 0 0]
arr := [5]int{1, 2, 3, 4, 5}
arr := [...]int{1, 2, 3, 4, 5} // length inferred from initialiser
fmt.Println(len(arr)) // 5
fmt.Println(arr[2]) // 3
arr[0] = 100
// Two arrays of different sizes have different types:
var a [3]int
var b [4]int
// a = b // ERROR: type mismatch
Arrays are value types — assignment copies all elements:
a := [3]int{1, 2, 3}
b := a // copy
b[0] = 100
fmt.Println(a) // [1 2 3] (unchanged)
fmt.Println(b) // [100 2 3]
For dynamic-size data, slices are the conventional choice; arrays are rare in idiomatic Go.
Slices
A slice is a dynamically-sized view into a backing array:
var s []int // nil slice; len 0, cap 0
s := []int{1, 2, 3} // literal
s := make([]int, 5) // length 5, cap 5
s := make([]int, 5, 10) // length 5, cap 10
fmt.Println(len(s)) // length
fmt.Println(cap(s)) // capacity
s[0] = 99 // index access
A slice has three components: a pointer to the backing array, a length, and a capacity. The mechanism admits substantial flexibility:
v := []int{1, 2, 3, 4, 5}
s2 := v[1:4] // [2, 3, 4] — len 3
s3 := v[:3] // [1, 2, 3] — first 3
s4 := v[2:] // [3, 4, 5] — from index 2
s5 := v[:] // entire slice
// Slices share the backing array:
s2[0] = 99
fmt.Println(v) // [1 99 3 4 5]
append
append adds elements; it may reallocate the backing array:
s := []int{1, 2, 3}
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6, 7) // [1 2 3 4 5 6 7]
// Concatenate slices:
a := []int{1, 2, 3}
b := []int{4, 5, 6}
c := append(a, b...) // [1 2 3 4 5 6]
The conventional pattern: assign the result back to the same name. The result may share or not share the backing array depending on capacity.
copy
copy copies elements between slices:
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src) // n is min(len(dst), len(src))
The form is conventional for “deep” slice copying. For Go 1.21+, the slices.Clone function is conventionally clearer:
import "slices"
clone := slices.Clone(src)
Slice operations
// Remove element at index i (preserving order):
s = append(s[:i], s[i+1:]...)
// Remove element at index i (faster, doesn't preserve order):
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// Insert element at index i:
s = append(s[:i], append([]int{value}, s[i:]...)...)
// Reverse:
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
The Go 1.21+ slices package provides many of these as functions:
import "slices"
slices.Reverse(s)
slices.Sort(s)
slices.Index(s, value)
slices.Contains(s, value)
slices.Equal(a, b)
slices.Clone(s)
slices.Insert(s, i, value)
slices.Delete(s, i, j)
Common pitfalls
// Aliasing:
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // shares backing array
b[0] = 99
fmt.Println(a) // [1 99 3 4 5]
// Memory retention:
big := makeBigSlice()
small := big[0:10] // backing array still includes the rest
// GC cannot reclaim the rest while small is alive
// Defence: full copy
small := append([]int{}, big[0:10]...) // independent copy
// or:
small := slices.Clone(big[0:10]) // Go 1.21+
Maps
A map is a hash table from keys to values:
m := map[string]int{} // empty map
m := make(map[string]int) // empty map
m := map[string]int{
"alice": 30,
"bob": 25,
"charlie": 35,
}
m["alice"] = 31 // insert/update
v := m["alice"] // get; returns zero value if missing
// Two-value form:
v, ok := m["dave"]
if !ok {
fmt.Println("not present")
}
delete(m, "alice") // remove
fmt.Println(len(m)) // size
// Iteration (order is randomised):
for k, v := range m {
fmt.Printf("%s = %d\n", k, v)
}
Nil maps
A nil map admits read access (returning zero values) but panics on write:
var m map[string]int // nil map
v := m["key"] // OK; returns 0
ok := m["key"] // OK
// m["key"] = 1 // PANIC: assignment to entry in nil map
The conventional defence is to initialise with make:
m := make(map[string]int)
Map keys
Any comparable type may be a map key — basic types, strings, pointers, channels, structs whose fields are all comparable, and interfaces. Slices, maps, and functions cannot be map keys:
m1 := map[string]int{} // OK
m2 := map[int]string{} // OK
m3 := map[Point]string{} // OK if Point's fields are comparable
m4 := map[[]int]int{} // ERROR: slice not comparable
m5 := map[interface{}]int{} // OK; runtime panic on incomparable values
Map iteration order
Map iteration order is randomised — Go deliberately produces different orders to discourage code that depends on order:
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Println(k, v) // order varies
}
For ordered iteration, sort the keys:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
The Go 1.21+ maps package provides:
import "maps"
clone := maps.Clone(m)
maps.Copy(dst, src) // copies src into dst
maps.Equal(a, b)
maps.DeleteFunc(m, func(k string, v int) bool { return v < 0 })
Structs
Aggregate types with named fields:
type Point struct {
X, Y float64
}
type Person struct {
Name string
Age int
Email string
Friends []string
}
// Construction:
p := Point{X: 1.0, Y: 2.0}
p := Point{1.0, 2.0} // positional; less robust
p := Point{} // zero value; X=0, Y=0
person := Person{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
Friends: []string{"Bob", "Charlie"},
}
// Field access:
fmt.Println(p.X, p.Y)
p.X = 10.0 // mutate
Structs are value types — assignment copies all fields:
a := Point{X: 1, Y: 2}
b := a // copy
b.X = 99
fmt.Println(a.X) // 1 (unchanged)
For pointer semantics, use *Struct:
p := &Point{X: 1, Y: 2}
q := p // pointer copy
q.X = 99
fmt.Println(p.X) // 99 (shared)
Anonymous structs
config := struct {
Host string
Port int
}{
Host: "localhost",
Port: 8080,
}
// Or for one-off literals:
type Result struct {
Status int
Body []byte
}
The anonymous form is conventional for one-off configurations and JSON deserialisation targets.
Struct tags
Struct fields admit tags — string metadata used by reflection-based libraries:
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Age int `json:"age" validate:"min=0"`
}
The conventional uses are JSON encoding (encoding/json), validation (validator), database mapping (gorm, sqlx), and ORM-style libraries.
Embedding
Embedding admits composition:
type Animal struct {
Name string
}
func (a Animal) Describe() string {
return fmt.Sprintf("I am %s", a.Name)
}
type Dog struct {
Animal // embedded
Breed string
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name) // promoted: "Rex"
fmt.Println(d.Describe()) // promoted: "I am Rex"
fmt.Println(d.Animal.Name) // explicit: "Rex"
Treated in Methods and interfaces.
Comparison
Structs are comparable if all their fields are comparable:
a := Point{X: 1, Y: 2}
b := Point{X: 1, Y: 2}
fmt.Println(a == b) // true
type WithSlice struct {
items []int
}
// w1 == w2 // ERROR: slice not comparable
For deep equality, the reflect.DeepEqual function:
import "reflect"
reflect.DeepEqual(a, b)
The form is reflective and slow; for performance, equality methods are conventional.
Standard library collections
The Go standard library provides additional structures:
container/list
A doubly-linked list:
import "container/list"
l := list.New()
l.PushBack(1)
l.PushBack(2)
l.PushFront(0)
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
The package is rarely used in idiomatic Go; slices serve most “list” needs.
container/heap
A priority queue (min-heap):
import "container/heap"
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
h := &IntHeap{2, 1, 5}
heap.Init(h)
heap.Push(h, 3)
fmt.Println(heap.Pop(h)) // 1
The mechanism is verbose; the conventional discipline is to wrap it in a typed structure if frequently used.
sort
Sorting:
import "sort"
s := []int{3, 1, 4, 1, 5, 9}
sort.Ints(s) // ascending
sort.Sort(sort.Reverse(sort.IntSlice(s))) // descending
// Slice with custom comparison:
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
// Search:
idx := sort.SearchInts(s, 5) // binary search
For Go 1.21+, the slices package provides:
import "slices"
slices.Sort(s) // ascending
slices.SortFunc(people, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
Common patterns
Slice as stack
stack := []int{}
stack = append(stack, 1) // push
stack = append(stack, 2)
top := stack[len(stack)-1] // peek
stack = stack[:len(stack)-1] // pop
Slice as queue (small queues)
queue := []int{}
queue = append(queue, 1) // enqueue
queue = append(queue, 2)
front := queue[0] // peek
queue = queue[1:] // dequeue (efficient via slice header)
For substantial queues, container/list or a circular buffer is conventional.
Set via map
Go has no built-in set; the conventional substitute is map[T]struct{}:
seen := map[string]struct{}{}
seen["a"] = struct{}{}
seen["b"] = struct{}{}
if _, ok := seen["a"]; ok {
fmt.Println("a is in")
}
The struct{} is the empty struct — zero memory. The conventional alternative for simpler code is map[T]bool:
seen := map[string]bool{
"a": true,
"b": true,
}
if seen["a"] {
fmt.Println("a is in")
}
Counter
counts := map[string]int{}
for _, word := range words {
counts[word]++
}
The increment of a missing key produces 1 (the zero value plus 1).
Group by
type Person struct {
Name string
City string
}
byCity := map[string][]Person{}
for _, p := range people {
byCity[p.City] = append(byCity[p.City], p)
}
The pattern leverages the zero-value-is-usable property: byCity[p.City] returns nil for absent keys, and append to nil works.
Default value lookup
v, ok := m[key]
if !ok {
v = defaultValue
}
// Or, with the zero value as the default:
v := m[key] // zero value if missing
Iteration patterns
// Stable iteration via sorted keys:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s = %v\n", k, m[k])
}
// Filter and transform:
result := make([]int, 0, len(s))
for _, x := range s {
if x > 0 {
result = append(result, x*2)
}
}
Pre-allocate when size is known
// Inefficient (multiple allocations):
result := []int{}
for _, x := range data {
result = append(result, transform(x))
}
// Efficient (single allocation):
result := make([]int, 0, len(data))
for _, x := range data {
result = append(result, transform(x))
}
A note on the conventional discipline
The contemporary Go data-structure advice:
- Use slices by default for sequences.
- Use maps for key-value lookup.
- Use structs for named-field aggregates.
- Use
makewith capacity for slices and maps when size is known. - Use the zero value — many types are usable without explicit initialisation.
- Use
map[T]struct{}for sets. - Use struct tags for serialisation (JSON, etc.).
- Use embedding for composition.
- Use
slicesandmaps(Go 1.21+) for the conventional operations. - Sort keys for deterministic map iteration.
- Use
sort.Sliceorslices.SortFuncfor custom-ordered sorting.
The combination — slices, maps, structs as the foundational types, the standard library packages for additional structures, embedding for composition, the zero value as the conventional default — is the substance of Go’s data-structure surface. The discipline produces clear, transparent code with substantial built-in functionality.