Polyglot
Languages Go data structures
Go § data-structures

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 make with 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 slices and maps (Go 1.21+) for the conventional operations.
  • Sort keys for deterministic map iteration.
  • Use sort.Slice or slices.SortFunc for 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.