Pointers to Structs

How to create and work with pointers to structs in Go, including automatic field dereferencing, modification, and common pitfalls.

A pointer to a struct is a variable that stores the memory address of a struct value. Instead of holding the struct’s fields directly, it tells you where in memory the struct lives. This indirection gives you the ability to share a single struct instance across many parts of your program without copying it, and any change made through the pointer is visible to everyone holding that same address.

The syntax *StructType declares a pointer to a struct, just as *int declares a pointer to an integer. The real power comes when you access the struct’s fields—Go makes this seamless by automatically dereferencing the pointer for you.

Why Pointers to Structs Are Necessary

A struct in Go is a value type. Passing a struct to a function or assigning it to another variable copies every field. For small structs that contain only a few fields, copying is cheap. But when structs grow large—think configuration blobs, database rows with dozens of columns, or deeply nested structures—duplicating them repeatedly wastes memory and time.

A pointer to a struct avoids that copy. It passes around only the address (8 bytes on a 64-bit machine), no matter how large the struct is. Beyond performance, pointers let you modify the original struct from a function or method. If you pass a struct by value and change a field inside the function, the caller sees no change. With a pointer, the caller sees the update immediately.

There is one more reason: consistency with method receivers. Many methods on structs are defined with pointer receivers so they can mutate the receiver. When you have a pointer to a struct, calling those methods is natural and efficient.

Two Ways to Obtain a Pointer to a Struct

Go gives you two common approaches to get a pointer to a struct. They are interchangeable in practice, but each is convenient in a different setting.

Using the address-of operator on an existing value

You already have a struct value and you want its address. The & operator gives you that pointer.

type Config struct {
    Port    int
    Timeout int
}
func main() {
    cfg := Config{Port: 8080, Timeout: 30}
    cfgPtr := &cfg   // cfgPtr is of type *Config
    fmt.Println(cfgPtr.Port) // 8080
}

Using the new built-in

The new function allocates memory for a struct, zeroes all its fields, and returns a pointer to it. You do not need an existing struct value first.

cfgPtr := new(Config)  // *Config with Port=0, Timeout=0
cfgPtr.Port = 3000
fmt.Println(cfgPtr.Port) // 3000

new vs &:

There is no performance difference between new(Config) and &Config{} — both allocate zero-valued memory and return a pointer. The &Config{} form is more common in idiomatic Go because it can also initialize fields in the same line: &Config{Port: 8080}. Use whichever reads more clearly.

A third, equally common form is to take the address of a composite literal directly:

cfgPtr := &Config{Port: 8080, Timeout: 30}

This creates the struct value and returns its pointer in one expression. You will see this pattern frequently in production code.

Automatic Dereferencing and the p.field Syntax

When you have a pointer to a struct, accessing a field with ptr.field feels like you are working with the struct itself. Under the hood, Go automatically dereferences the pointer—you do not have to write (*ptr).field.

type User struct {
    Name string
    Age  int
}
func main() {
    u := User{Name: "Alice", Age: 25}
    p := &u
    // These two lines do exactly the same thing.
    // Go allows the shorter form.
    fmt.Println(p.Name)         // Alice
    fmt.Println((*p).Name)     // Alice
}

This automatic dereferencing is a deliberate language choice to reduce visual noise. It works everywhere: in assignments, comparisons, and even when chaining deeper into nested structs or struct fields that are themselves pointers.

The dot operator works with both values and pointers:

If you have a *User variable p and call p.Name, Go resolves it as if it were (*p).Name. This is why you can write p.Name without ever thinking about dereference syntax once the pointer is created. The language treats a pointer to a struct as transparently as possible.

Modifying Structs Through Pointers

When you modify a field through a pointer, the change goes directly to the original struct because the pointer points to that original memory. No copy is made.

type Account struct {
    Balance float64
}
func deposit(acc *Account, amount float64) {
    acc.Balance += amount  // modifies the original struct
}
func main() {
    a := Account{Balance: 100}
    deposit(&a, 50)
    fmt.Println(a.Balance) // 150
}

Passing the address allows deposit to update the caller’s a directly. If the function took an Account value instead, a.Balance would still be 100 after the call because only a copy would have been modified.

Nil pointer dereference panics at runtime:

If you declare a pointer to a struct but never assign an actual struct to it, the pointer is nil. Trying to access a field on a nil pointer panics:

var p *User   // p is nil
fmt.Println(p.Name) // panic: runtime error: invalid memory address or nil pointer dereference

Always initialize the pointer before use—either with new, &User{}, or by assigning an existing address. The compiler cannot catch this at build time, so it is a common source of crashes for newcomers.

Choosing Between a Struct Value and a Pointer to Struct

The decision does not have a single correct answer; it depends on how the struct is used. Here are some heuristics that help in everyday Go code.

  • Small, short-lived data: A struct that contains only a few primitive fields and is discarded soon after creation (like a JSON request body or a configuration snapshot) works fine as a value. The copy is cheap, and the code is simpler because you never risk nil dereferences.
  • Large structs or structs with many fields: If the struct grows in size over time (think a UserProfile with dozens of fields and nested slices), passing a pointer avoids repeated copying. Even if you don’t need mutation, a pointer can be more efficient.
  • Mutation requirement: If you need changes to be visible to the caller or multiple parts of the program, a pointer is mandatory. Values cannot propagate changes back to the original.
  • Nil-ability as a semantic signal: A pointer can be nil, meaning “no struct present.” This is often used to represent optional data, though it comes with the nil-check burden. A struct value cannot be nil; its zero value always exists.
  • Interface and method set considerations: Methods with pointer receivers are only callable on a pointer to the struct. If you plan to call such methods, you need a pointer.

Copying a struct that contains a slice or map field does not deep-copy those internal references:

If a struct has a slice field and you copy the struct by value, both the original and the copy point to the same underlying array. Changes through one will be visible through the other—even though you thought you were working with a safe copy. When a struct contains reference types, a pointer to the struct often makes the ownership clearer: you are explicitly sharing a single instance.

Summary

Pointers to structs exist because Go treats structs as values and copying them can be wasteful or semantically wrong. A pointer gives you a light‑weight reference that avoids duplication and enables mutation. Go removes the usual syntactic overhead by automatically dereferencing pointers when you access fields, so working with a *Person feels almost identical to working with a Person.

The two main creation patterns—&existingValue and new(Type)—cover every scenario, and the compact &Type{...} idiom is the one you will write most often. The most important trap to watch for is a nil pointer: a pointer variable without an actual struct behind it will panic on any field access.

The same automatic dereferencing lets you call pointer‑receiver methods on a value if the value is addressable, but the underlying principle is the same: a pointer to a struct is your tool for sharing and modifying data without copying.