f in x
Go Types: struct, interface, slice, map, and pointer — Advanced Guide for Robust Code
> cd .. / HUB_EDITORIALE
Analisi dei dati e metriche

Go Types: struct, interface, slice, map, and pointer — Advanced Guide for Robust Code

[2026-06-04] Author: Ing. Calogero Bono

You're writing Go for a while. You use struct for data, slice for lists, map for dictionaries. It works. But as the project grows — state management, shared APIs, concurrency — trouble starts: unexpected mutations, interfaces that don't fit, types that break. The problem isn't Go: it's not understanding the memory model and the contract of each type.

At Meteora Web, we've been using Go for high-performance backends for years. We've seen code where passing a slice by value crashed the server, or a shared map between goroutines triggered data races. This guide is not a beginner's recap: it's a hands-on deep dive into how struct, interface, slice, map, and pointer really work, with examples you can compile and test right away.

Go's Memory Model: Value vs Reference

Before any type, you need to grasp one thing: in Go almost everything is passed by value. When you assign a variable to another or pass it to a function, Go copies the value. Sounds trivial, but it's the cause of 80% of errors.

The exceptions are types with implicit references: slice, map, channel, pointer (obviously), and interface. When you copy a slice or a map, you copy the data structure that points to the actual data, not the data itself. Two variables can share the same underlying array. This is powerful, but dangerous if you don't control it.

Struct: Aggregate Data, But Beware of Copy

A struct is a value type. Passing it to a function makes a complete copy — all fields, bytes. Fine for small structs. For structs with internal slices or large fields, use a pointer.

type Person struct {
    Name string
    Age  int
}

func birthday(p Person) {
    p.Age++
}

func main() {
    p := Person{Name: "Mario", Age: 30}
    birthday(p)
    fmt.Println(p.Age) // 30, not 31!
}

Solution: pass a pointer.

func birthday(p *Person) {
    p.Age++
}

Methods on Struct: Value vs Pointer Receiver

Methods in Go can have a value or pointer receiver. The choice changes everything: a value receiver copies the struct; a pointer one does not. Rule of thumb: if the method modifies state, use pointer receiver. For small structs (e.g., time.Duration), value is fine.

func (p *Person) HaveBirthday() {
    p.Age++
}

func (p Person) IsAdult() bool {
    return p.Age >= 18
}

Slice: The Hidden Structure

A slice is not an array. It's a three-field struct: pointer to underlying array, length, and capacity. When you pass a slice to a function, you pass that triplet (by value). But the pointer to the array remains: if you modify elements, you modify the shared array.

Classic mistake:

func modifySlice(s []int) {
    for i := range s {
        s[i] *= 2
    }
    s = append(s, 99) // does not modify original slice!
}

func main() {
    a := []int{1, 2, 3}
    modifySlice(a)
    fmt.Println(a) // [2 4 6] — not [2 4 6 99]
}

To modify the slice (add/remove), return it or pass a pointer to slice.

Slice Capacity and Append

When you append, if capacity is enough, the slice writes to the same memory area. Otherwise it allocates a new one (and doubles capacity). Knowing this avoids unnecessary allocations.

s := make([]int, 0, 10) // capacity 10
for i := 0; i < 10; i++ {
    s = append(s, i)    // no allocations
}

Map: Reference Type to Handle with Care

A map in Go is a pointer to an internal structure. Passing it by value equals passing a pointer: if you modify the content, both sides see it. But if you assign nil to one variable, the other doesn't know.

func addEntry(m map[string]int, key string, val int) {
    m[key] = val
}

func main() {
    m := map[string]int{"a": 1}
    addEntry(m, "b", 2)
    fmt.Println(m) // map[a:1 b:2]
}

Warning: maps are not thread-safe. If two goroutines write to the same map without synchronization, you get fatal error: concurrent map writes. Use sync.Mutex or sync.Map.

Map and Pointers: Beware of Copy

When a map has struct values, getting v := m[key] copies the struct. Modifying v does not update the map. Solution: either use pointer values or reassign.

type Counter struct{
    Value int
}

func main() {
    m := map[string]*Counter{}
    m["page"] = &Counter{}
    m["page"].Value++ // ok because it's a pointer
}

Pointer: Explicit Control

Pointers in Go are safe (no pointer arithmetic like C), but must be used with awareness. A pointer can be nil. Dereferencing a nil pointer causes a panic.

Use pointers when:

  • You need to modify a value from a function (e.g., Scan(&x))
  • The data structure is large and you want to avoid copies
  • You need to represent absence of a value (e.g., var p *int = nil)

Don't use pointers for small immutable values (int, bool). Pointers add unnecessary complexity and increase garbage collector pressure.

Interface: The Implicit Contract

An interface in Go is a type that holds two pointers: the dynamic type and the dynamic value. When you assign a value to an interface, Go stores the type and a copy of the value (or a pointer if the value is a pointer).

Interfaces are satisfied implicitly: if a type implements the declared methods, it automatically implements the interface. This is flexible but can lead to subtle errors.

Nil is Not Always Nil

An interface variable is nil only if both type and value are nil. If you assign a nil pointer to an interface, the interface is not nil — it has a type (*Person) and a nil value. This breaks nil checks.

var p *Person = nil
var i interface{} = p
fmt.Println(i == nil) // false!

Moral: don't check err != nil if err might be an interface wrapping a nil value. Better to return explicit nil for the interface.

Empty Interface and Type Assertion

Empty interface interface{} accepts any type. Don't use it as wild dynamic typing — Go is not JavaScript. For heterogeneous data, use any (alias in Go 1.18+) and type switches.

func detect(v any) {
    switch val := v.(type) {
    case int:
        fmt.Println("int:", val)
    case string:
        fmt.Println("string:", val)
    default:
        fmt.Println("unknown")
    }
}

When to Use Pointers and When Values in Compound Types

No one-size-fits-all rule. At Meteora Web we follow these guidelines:

  • Structs representing entities with identity (e.g., user, order): use pointers or always use pointer (*User).
  • Small immutable structs (e.g., coordinates, color): pass by value.
  • Slice of structs: if you need to modify elements in-place, use []*T. Otherwise []T is more cache-friendly.
  • Maps of structs: almost always map[K]*V to avoid copies.
  • Interfaces: avoid storing pointers to interfaces. Interfaces are already pointers to the actual value.

What To Do Next

  1. Review your Go code — look for passing slices and maps to functions, and ask: “what happens if I modify here?”.
  2. Test the difference between value and pointer receiver on a medium-sized struct (e.g., 20 fields). Compare allocations with go test -benchmem.
  3. Apply type assertions cautiously: never do v.(T) without the second boolean, or use a switch.
  4. Design interfaces for behavior, not data. A single-method interface (Reader, Writer) is gold.
  5. Mutex shared maps between goroutines. Or better, use sync.Map for specific cases (heavy reads, rare writes).

We at Meteora Web have seen Go codebases turn into nightmares when these mechanisms are ignored. But with the right awareness, they become incredibly powerful tools. Happy coding.

Sponsored Protocol

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Co-founder di Meteora Web. Ingegnere informatico, sviluppo ecosistemi digitali ad alte prestazioni. AI, automazione, SEO tecnica e infrastrutture web. Scrivo di tecnologia per rendere complesso… semplice.

[ Read Full Dossier ]

Hai bisogno di applicare questa strategia?

Esegui il protocollo di contatto per iniziare un progetto con noi.

> INIZIA_PROGETTO

Sponsored

> MW_JOURNAL

> READ_ALL()