Value vs pointer

The previous chapter introduced pointers with primitives. Structs are where the value-versus-pointer choice starts paying rent. Almost every function signature in a real Go codebase makes this decision; getting it right is one of the handful of choices that separates code that compiles from code that behaves the way the author intended.

Passing a struct by value copies every field

When you pass a struct to a function by value, the function receives a full copy of the struct. Changes inside the function do not touch the caller's original:

type User struct {
    Name  string
    Email string
}

func rename(u User, to string) {
    u.Name = to
}

func main() {
    alice := User{Name: "Alice", Email: "alice@example.com"}
    rename(alice, "Alice Smith")

    fmt.Println(alice.Name)   // Alice   (unchanged!)
}

Same rule you met with primitive ints in the previous chapter, now with a struct on board. The function's u is a local copy; writing to u.Name only changes the copy.

Pass a pointer to mutate the caller's struct

If you want the function to modify the caller's struct, take *User and write through the pointer:

func rename(u *User, to string) {
    u.Name = to
}

func main() {
    alice := User{Name: "Alice", Email: "alice@example.com"}
    rename(&alice, "Alice Smith")

    fmt.Println(alice.Name)   // Alice Smith
}

Three changes from the broken version:

  • The parameter is *User instead of User.
  • The call site passes &alice.
  • The body uses u.Name = to. Note there is no *u in sight, even though u is a pointer.

Automatic dereference through .

That no-*u rule is worth stating directly. For struct fields, the dot operator automatically dereferences a pointer. u.Name and (*u).Name mean the same thing:

u := &User{Name: "Alice"}

fmt.Println(u.Name)        // Alice, same as (*u).Name
u.Name = "Bob"              // writes through the pointer

You will see u.Name everywhere in Go code; you will almost never see (*u).Name. The language designers wanted struct access to read the same whether you have a value or a pointer, so the . does the work for you.

Constructing a pointer to a struct

The Pointers chapter introduced three ways to get a pointer. For structs, only one matters in practice: the composite-literal address, &T{...}:

u := &User{
    Name:  "Alice",
    Email: "alice@example.com",
}

&User{...} builds the struct and returns a pointer to it in one expression. This is the shape you will see in every real Go codebase. new(User) would give you a pointer to a zero-valued User with no fields set; useful only when you genuinely want the zero value.

Deciding between value and pointer for a struct type

There are two reasons to take a pointer, spelled out in the Pointers chapter: to let a function modify the caller's value, and to avoid the cost of copying a large value. Structs are where both reasons show up concretely. Some guidance that holds up in real code:

  • Types that represent shared identity take a pointer. A User, an Order, a Connection, an http.Request, a *sql.DB: each one logically has "the one that matters" in a given context, and every function that handles one should see changes through it. Pointers make that sharing explicit and cheap.

  • Small value-flavoured types take a value. A Point, a time.Time, an RGB colour, a Money amount: fine to copy, often immutable in spirit, and do not benefit from shared identity. Passing by value keeps functions easy to reason about.

  • Big types (rough rule: more than six or eight fields, or containing arrays) usually take a pointer even for read-only work, just to avoid the copy cost on every call. You can see the size difference directly:

    import "unsafe"
    
    type BigRecord struct {
        ID        int64
        Name      string
        Email     string
        Phone     string
        Address   string
        City      string
        Country   string
        CreatedAt int64
        UpdatedAt int64
        Fingerprint [32]byte
    }
    
    var b BigRecord
    fmt.Println(unsafe.Sizeof(b))    // ~192 bytes (exact number depends on alignment)
    fmt.Println(unsafe.Sizeof(&b))   // 8 bytes on a 64-bit machine
    

    Every call that passes b by value copies all those bytes into the callee's frame. Passing &b copies one machine word, regardless of how much data the struct carries. The ratio matters less for small types (copying a 16-byte Point is effectively free) and a lot for large ones inside hot loops.

  • Be consistent within a single type. If one function on User takes *User, every function on User should, even the read-only ones. Mixing pointer and value signatures on the same type leads to subtle bugs (more on this when you meet methods next chapter).

The practical default for non-trivial structs is pointer

When in doubt, take *T. Most real Go code uses pointers for its domain types (*User, *Order, *Request) and plain values only for small, arithmetic-flavoured types (Point, time.Duration, Money). Start with the pointer form and only peel back to a value when you have a specific reason: immutability, cheap copy semantics, or using the type as a map key (covered in the Comparing structs lesson).

How this shapes a real codebase

In most Go services, the big domain types live in their own package, are created with a constructor (NewUser(...) that returns *User), and travel through the system as pointers. HTTP handlers take *http.Request and return responses through http.ResponseWriter; database rows are scanned into *User; business logic functions take *Order and mutate it. You hardly ever see bare User values for these types, because changes to them need to be visible to every caller.

Small value types (a Range, a Duration, a Money) travel by value. They are built with literals and compared with == and do not have a single "canonical" instance.

This split will feel natural after a few weeks of writing Go. For now, the pattern worth memorising is: big-deal domain types → *T; small value-flavoured types → T.

Task

The starter defines:

type Counter struct {
    Name  string
    Value int
}

Write two functions:

  • inc(c *Counter) that increments c.Value by one through the pointer. It must modify the caller's counter.
  • snapshot(c Counter) Counter that takes a counter by value and returns a new counter with the same Name but Value doubled. It must not modify the caller's counter.

In main, the starter already:

  • Builds hits := &Counter{Name: "hits", Value: 5}.
  • Calls inc(hits).
  • Calls snap := snapshot(*hits).
  • Prints hits.Value, then snap.Value.

With your implementation, the expected output is:

6
12

hits.Value went from 5 to 6 because inc modified it through the pointer. snap.Value is 12 because snapshot received a copy of the counter (at Value: 6) and doubled it. The copy's doubling did not leak back into hits.

Expected output
6
12