Structs
fields ยท methods ยท receivers
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
promoted fields ยท compositionGo 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
fixed size ยท value typeArrays 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
make ยท append ยท copy ยท len/cap 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
make ยท CRUD ยท comma-ok ยท delete
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
implicit ยท duck typing ยท nil interfaceGo 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
.(T) ยท comma-ok ยท type switch
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
type params ยท constraints ยท Go 1.18+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
Syntax cheat-sheet| Concept | Syntax | Notes |
|---|---|---|
| Struct literal | User{Name: "Alice", Age: 30} | Named form โ preferred |
| Anonymous struct | struct{ X, Y int }{1, 2} | Good for one-off shapes & test cases |
| Value receiver | func (r Rect) Area() float64 | Gets a copy; cannot mutate |
| Pointer receiver | func (r *Rect) Scale(f float64) | Mutates original; use for large structs |
| Embedding | type 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 slice | make([]T, len, cap) | cap optional, defaults to len |
| append | s = append(s, v) | Always reassign โ may return new slice |
| Spread append | append(s, other...) | Appends all elements of other |
| copy | copy(dst, src) | Independent data; stops at shorter |
| Sub-slice | s[lo:hi] | Shares array; use s[lo:hi:max] to limit cap |
| Map literal | map[string]int{"a": 1} | Reference type; nil map panics on write |
| make map | make(map[K]V) | Optional capacity hint: make(map[K]V, n) |
| Comma-ok map | v, ok := m[key] | ok=false if key absent |
| delete | delete(m, key) | No-op if key doesn't exist |
| Set pattern | map[T]struct{}{} | Zero-size value; no wasted memory |
| Interface | type S interface { M() } | Satisfied implicitly |
| Type assertion | v, ok := x.(T) | Comma-ok preferred; panics without |
| Type switch | switch v := x.(type) | Dispatches on concrete type |
| Generic function | func F[T any](v T) | Type inferred at call site |
| comparable | func F[T comparable] | Allows == and != on T |
| Union constraint | ~int | ~float64 | ~ includes derived types |
| Generic type | type Stack[T any] struct{} | Instantiate: Stack[int] |