Go Data Structures

Structs, embedding, slices, maps, interfaces, type assertions, and generics โ€” the building blocks of every Go program.

Structs Embedding Slices Maps Interfaces Type assertion Generics
๐Ÿ—๏ธ

Structs

Definition, literals & field access struct
type User struct {
    Name  string
    Age   int
    Email string
    Admin bool
}

// Named literal โ€” preferred, order-independent
u := User{Name: "Alice", Age: 30, Email: "alice@example.com"}

// Positional literal โ€” fragile, avoid for >2 fields
u2 := User{"Bob", 25, "bob@example.com", false}

// Field access
fmt.Println(u.Name)  // "Alice"
u.Age = 31

// Anonymous struct โ€” useful for one-off shapes (JSON, test cases)
point := struct{ X, Y int }{X: 1, Y: 2}

// Struct is a value type โ€” assignment copies all fields
u3 := u
u3.Name = "Carol"
fmt.Println(u.Name)  // still "Alice"
Methods โ€” value vs pointer receivers Methods
type Rect struct{ Width, Height float64 }

// Value receiver โ€” gets a copy, cannot mutate the original
func (r Rect) Area() float64 {
    return r.Width * r.Height
}

// Pointer receiver โ€” operates on the original, can mutate it
func (r *Rect) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

rect := Rect{Width: 4, Height: 3}
fmt.Println(rect.Area()) // 12
rect.Scale(2)
fmt.Println(rect.Area()) // 48

// Go auto-takes the address: rect.Scale(2) is (&rect).Scale(2)
๐Ÿ’ก
Pick one receiver type per struct and be consistent. Use pointer receivers when the method mutates state, the struct is large (avoids copying), or you need consistent behaviour with other pointer-receiver methods.
๐Ÿงฉ

Embedding

โ„น๏ธ
Go favours composition over inheritance. Embedding a type promotes its fields and methods to the outer struct โ€” no subclassing needed.
Promoted fields & methods Embed
type Animal struct {
    Name string
}

func (a Animal) Speak() string { return a.Name + " speaks" }

type Dog struct {
    Animal        // embedded โ€” no field name
    Breed string
}

d := Dog{
    Animal: Animal{Name: "Rex"},
    Breed:  "Labrador",
}

// Promoted โ€” access Animal fields/methods directly on Dog
fmt.Println(d.Name)      // "Rex"    (d.Animal.Name)
fmt.Println(d.Speak())  // "Rex speaks"
fmt.Println(d.Breed)     // "Labrador"
Overriding promoted methods Override
type Cat struct {
    Animal
}

// Define a method with the same name โ€” shadows the embedded one
func (c Cat) Speak() string { return c.Name + " meows" }

cat := Cat{Animal: Animal{Name: "Whiskers"}}
fmt.Println(cat.Speak())        // "Whiskers meows" โ€” Cat's method
fmt.Println(cat.Animal.Speak()) // "Whiskers speaks" โ€” explicit call

// Embedding also works with interfaces โ€” a common pattern
type ReadWriter interface {
    io.Reader  // embeds Reader interface
    io.Writer  // embeds Writer interface
}
๐Ÿ“

Arrays

โš ๏ธ
Arrays are value types โ€” assigning or passing an array copies the entire thing. Their size is part of their type: [3]int and [4]int are different types. In practice, you'll use slices almost everywhere; arrays appear mainly as the backing store for slices or for fixed-size data like SHA hashes.
Declaration & initialisation array
// Explicit size
var a [3]int              // [0 0 0]
a[0] = 10

// Literal
b := [3]int{1, 2, 3}

// Let compiler count with ...
c := [...]string{"foo", "bar", "baz"}  // [3]string

// Sparse initialisation โ€” unset elements stay zero
d := [5]int{1: 10, 4: 50}             // [0 10 0 0 50]

fmt.Println(len(c)) // 3

// Arrays are comparable if their element type is comparable
[2]int{1, 2} == [2]int{1, 2}  // true
๐Ÿ—‚๏ธ

Slices

  s := make([]int, 3, 5)    // len=3, cap=5

  slice header                   underlying array
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”
  โ”‚  ptr   โ”‚ len โ”‚ cap โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚  0 โ”‚  0 โ”‚  0 โ”‚    โ”‚    โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜
              3     5            โ•ฐโ”€โ”€โ”€ len โ”€โ”€โ”€โ•ฏโ•ฐโ”€โ”€โ”€ cap โ”€โ”€โ”€โ•ฏ

  append stays in-place while len < cap. Once cap is exceeded,
  Go allocates a new, larger array and copies โ€” the original is untouched.
make, append & slicing Slice
// make([]T, len, cap) โ€” cap is optional, defaults to len
s := make([]int, 3)       // [0 0 0]  len=3 cap=3
s  = make([]int, 3, 10)   // [0 0 0]  len=3 cap=10

// Literal
nums := []int{10, 20, 30, 40, 50}

// append โ€” returns a (possibly new) slice
nums = append(nums, 60)
nums = append(nums, 70, 80)     // multiple at once
more := []int{90, 100}
nums = append(nums, more...)  // spread another slice

// Slicing โ€” shares the underlying array
a := nums[1:4]   // elements 1,2,3 โ€” [20 30 40]
b := nums[:3]    // from start    โ€” [10 20 30]
c := nums[3:]    // to end        โ€” [40 50 ...]

// Writing through a sub-slice affects the original
a[0] = 99
fmt.Println(nums[1]) // 99
copy โ€” independent data copy
// copy(dst, src) returns elements copied
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)

dst[0] = 99
fmt.Println(src[0]) // 1 โ€” unaffected
fmt.Println(dst[0]) // 99

// copy stops at the shorter slice
n := copy(dst, []int{7, 8})
fmt.Println(n) // 2
nil vs empty slice nil
var s []int          // nil slice
e := []int{}         // empty slice

s == nil             // true
e == nil             // false

len(s) == 0          // true โ€” safe to range over
len(e) == 0          // true

// append works on a nil slice
s = append(s, 1, 2)  // [1 2]

// JSON: nil โ†’ null, empty โ†’ []
json.Marshal(s)  // [1,2]
json.Marshal(([]int)(nil)) // null
โš ๏ธ
Sub-slices share the underlying array. An append to a sub-slice that fits within the original's capacity will silently overwrite data further along the original. Use a 3-index slice s[lo:hi:max] to set the capacity of the sub-slice, forcing append to allocate a fresh array instead.
๐Ÿ—บ๏ธ

Maps

Create, read, update & delete map
// make or literal
ages := make(map[string]int)
ages = map[string]int{
    "alice": 30,
    "bob":   25,
}

// Write
ages["carol"] = 28

// Read โ€” missing key returns zero value, not an error
fmt.Println(ages["alice"])   // 30
fmt.Println(ages["nobody"])  // 0 โ€” zero value for int

// Comma-ok โ€” distinguish "key missing" from "value is zero"
v, ok := ages["alice"]   // v=30, ok=true
v, ok  = ages["nobody"]  // v=0,  ok=false

// Delete
delete(ages, "bob")
fmt.Println(len(ages)) // 2

// Iterate โ€” order is random on every run
for k, v := range ages {
    fmt.Printf("%s: %d\n", k, v)
}
Map of slices Pattern
// Group items by key
groups := make(map[string][]string)

add := func(k, v string) {
    groups[k] = append(groups[k], v)
}

add("fruit", "apple")
add("fruit", "banana")
add("veg",   "carrot")

// groups["fruit"] = ["apple" "banana"]
Set pattern Pattern
// Go has no built-in set โ€” use map[T]struct{}
seen := make(map[string]struct{})

add := func(s string) {
    seen[s] = struct{}{}
}
has := func(s string) bool {
    _, ok := seen[s]
    return ok
}

add("go")
has("go")   // true
has("rust") // false
โš ๏ธ
Writing to a nil map panics. Always initialise with make or a literal before writing. Reading from a nil map is safe and returns the zero value.
๐Ÿ”Œ

Interfaces

โ„น๏ธ
Go interfaces are satisfied implicitly โ€” no implements keyword. If a type has all the methods the interface requires, it satisfies it automatically. Define interfaces where they're consumed, not where the type is defined.
Defining & satisfying interfaces interface
type Stringer interface {
    String() string
}

type Color struct{ R, G, B uint8 }

// Color satisfies Stringer โ€” no declaration needed
func (c Color) String() string {
    return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}

func print(s Stringer) { fmt.Println(s.String()) }

print(Color{255, 128, 0}) // "#ff8000"

// Interface value holds (type, value) pair
var s Stringer = Color{0, 255, 0}
fmt.Printf("%T\n", s) // "main.Color"
Empty interface / any any
// any = interface{} โ€” holds any type
// Use sparingly; prefer typed interfaces
func printAny(v any) {
    fmt.Printf("%T: %v\n", v, v)
}

printAny(42)       // int: 42
printAny("hi")    // string: hi
printAny(true)    // bool: true

// Common in JSON decode targets
var m map[string]any
json.Unmarshal(data, &m)
nil interface gotcha Gotcha
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

func bad() error {
    var err *MyError = nil
    return err  // NOT nil! interface holds (*MyError, nil)
}

fmt.Println(bad() == nil) // false โ€” surprise!

// Fix: return nil directly
func good() error {
    return nil  // interface is truly nil
}
๐Ÿ”

Type Assertion

Asserting & the comma-ok form .(T)
var i any = "hello"

// Direct assertion โ€” panics if wrong type
s := i.(string)
fmt.Println(s) // "hello"

// Comma-ok โ€” safe, no panic
s, ok := i.(string)  // s="hello", ok=true
n, ok := i.(int)     // n=0,       ok=false

// Always prefer comma-ok unless you're certain of the type
if s, ok := i.(string); ok {
    fmt.Println("string value:", s)
}

// Asserting to an interface โ€” checks method set
type Closer interface{ Close() error }
if c, ok := i.(Closer); ok {
    c.Close()
}
๐Ÿ’ก
When you need to dispatch on multiple types, use a type switch (switch v := x.(type)) instead of chaining assertions โ€” it's cleaner and the compiler checks exhaustiveness better. See the Basics page for type switch examples.
๐Ÿงฌ

Generics

โ„น๏ธ
Generics (Go 1.18+) let you write functions and types that work across multiple types without sacrificing type safety or resorting to any. Use them when you find yourself duplicating logic that differs only by type.
Generic functions Go 1.18+
// [T any] โ€” T can be any type
func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

Map([]int{1, 2, 3}, func(n int) string {
    return strconv.Itoa(n)
})
// ["1" "2" "3"]

// comparable โ€” allows == and != operators
func Contains[T comparable](s []T, v T) bool {
    for _, item := range s {
        if item == v { return true }
    }
    return false
}

Contains([]string{"a", "b"}, "b") // true
Contains([]int{1, 2, 3}, 4)        // false
Constraints & generic types Constraints
// Interface as constraint โ€” restrict which types are allowed
type Number interface {
    ~int | ~int64 | ~float64
}

// ~ means "any type whose underlying type is T"
func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums { total += n }
    return total
}

Sum([]int{1, 2, 3})       // 6
Sum([]float64{1.1, 2.2}) // 3.3

// Generic type โ€” Stack[T]
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(v T)     { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    n := len(s.items) - 1
    v := s.items[n]
    s.items = s.items[:n]
    return v, true
}

var st Stack[int]
st.Push(1); st.Push(2)
v, _ := st.Pop()  // 2
๐Ÿ“‹

Quick Reference

Concept Syntax Notes
Struct literalUser{Name: "Alice", Age: 30}Named form โ€” preferred
Anonymous structstruct{ X, Y int }{1, 2}Good for one-off shapes & test cases
Value receiverfunc (r Rect) Area() float64Gets a copy; cannot mutate
Pointer receiverfunc (r *Rect) Scale(f float64)Mutates original; use for large structs
Embeddingtype Dog struct { Animal }Promotes fields & methods
Array literal[3]int{1, 2, 3}Size is part of the type; value type
Array inferred size[...]string{"a", "b"}Compiler counts elements
Slice literal[]int{1, 2, 3}Reference type; backed by array
make slicemake([]T, len, cap)cap optional, defaults to len
appends = append(s, v)Always reassign โ€” may return new slice
Spread appendappend(s, other...)Appends all elements of other
copycopy(dst, src)Independent data; stops at shorter
Sub-slices[lo:hi]Shares array; use s[lo:hi:max] to limit cap
Map literalmap[string]int{"a": 1}Reference type; nil map panics on write
make mapmake(map[K]V)Optional capacity hint: make(map[K]V, n)
Comma-ok mapv, ok := m[key]ok=false if key absent
deletedelete(m, key)No-op if key doesn't exist
Set patternmap[T]struct{}{}Zero-size value; no wasted memory
Interfacetype S interface { M() }Satisfied implicitly
Type assertionv, ok := x.(T)Comma-ok preferred; panics without
Type switchswitch v := x.(type)Dispatches on concrete type
Generic functionfunc F[T any](v T)Type inferred at call site
comparablefunc F[T comparable]Allows == and != on T
Union constraint~int | ~float64~ includes derived types
Generic typetype Stack[T any] struct{}Instantiate: Stack[int]