Go from the ground up

Structs and methods

Making your own types out of fields, hanging functions off them, and the one distinction that matters: a method that works on a copy, and a method that works on the real thing.

Part 5 of 12

Intermediate15 min readBack to Courses

A struct is a named set of fields

A struct gathers related values under one name: type User struct { Name string; Visits int }. Making one with field names is the habit to keep — User{Name: "Ada", Visits: 1} — because the day the struct gains a field, code written that way still reads correctly and code written positionally turns into a puzzle.

A struct with no fields is still useful: type Config struct{} can carry behaviour, and it can be passed around without any data. The zero value of a struct is a struct with every field at its zero value, which means a declared struct is immediately safe to use.

Methods hang off a named type

A method is a function with a receiver: func (u User) Label() string. The receiver appears before the name, and the method is then called with a dot — u.Label() — which reads as the thing it is: an action that belongs to the value.

The receiver can be the value, or a pointer to it. A value receiver gets a copy, so it cannot change the original and it cannot be nil. A pointer receiver gets the address, so it can change the fields — and it is the receiver to use when the method changes anything, or when the struct is large enough that copying it per call is waste.

Constructors, by convention

Go has no constructors in the language, only the habit: a function named NewUser that returns a User or a pointer to one. It is where defaults are set and where a struct that must not be used half-built gets built properly.

Returning a pointer from the constructor is common when the type has pointer-receiver methods or is expensive to copy. It is a choice about the value, not a rule — and it stays visible everywhere the type is used, which is Go’s usual way. A constructor that returns a value is copied at every call; one that returns a pointer shares one value. Neither is wrong, but the choice shows up in every assignment, so make it deliberately.

A struct that remembers

package main

import "fmt"

type Counter struct {
	Name  string
	Count int
}

func NewCounter(name string) *Counter {
	return &Counter{Name: name}
}

// Value receiver: works on a copy, reads only.
func (c Counter) Summary() string {
	return fmt.Sprintf("%s: %d", c.Name, c.Count)
}

// Pointer receiver: changes the value that called it.
func (c *Counter) Add(n int) {
	c.Count += n
}

func main() {
	visits := NewCounter("visits")
	visits.Add(3)
	visits.Add(4)
	fmt.Println(visits.Summary())

	copied := *visits
	copied.Add(100)
	fmt.Println("copy says:", copied.Summary())
	fmt.Println("original says:", visits.Summary())
}

A method call, step by step

Follow a call to see the receiver rule in action. visits.Add(3) evaluates visits, sees that Add has a pointer receiver, and passes the address — the compiler inserts the & because visits is an addressable variable. Inside, c is that pointer, so c.Count += 3 writes through it to the original struct. visits.Summary() evaluates the same value, sees a value receiver, and copies the struct into the call; the method reads the copy and cannot touch the original. If visits were an element of a map, the address would not exist to take, and the pointer-receiver call would be a compile error until you assigned the element to a variable first.

A little library

Run it, then add a method that lends a copy out and marks it as borrowed, and a field that counts how many times each book has been read.

Output
Click "Run" to execute your code

Pick one receiver kind per type and keep it. Mixing value and pointer receivers on the same struct is legal, but a type whose methods disagree about whether they change it is a type nobody can read confidently.

Value receivers copy, pointer receivers reach

A method receives its receiver according to the kind written in the signature. func (c Cart) Total() gets a snapshot: fields changed inside the method are changed on the copy and gone when it returns. func (c *Cart) Add() gets the address, so it edits the value that called it.

Go writes the & for you when the value is addressable: cart.Add() on a variable is (&cart).Add(). It cannot do that for a value that is not addressable — an element of a map, or a function result — so those need a variable of their own before a pointer-receiver method can be called.

Embedding is composition without a field name. type Shelf struct { Cart } promotes Cart's fields and methods, and Shelf satisfies any interface Cart satisfied through value receivers. It is not inheritance: the outer type does not become the inner one, and a method on the outer type simply takes precedence.

A range loop gives you a copy: for _, item := range cart.Items { item.Price = 0 } changes nothing. Write through the index, cart.Items[i].Price = 0. Copying a struct also copies its slice and map fields by reference, so the copy still shares their contents — and a struct holding a sync.Mutex must never be copied after use.

Try these next

  • Add one value-receiver method and one pointer-receiver method to the same type, then print the value before and after each call.
  • Copy a Cart, change the copy with a pointer-receiver method, and show the original is untouched.
  • Embed a struct, then call a promoted field and method through the outer type.

A cart with a running total

Run it, then add a third item and a larger discount. Add a method that reports the average price per item, and decide whether it needs a pointer receiver.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Which receiver do you use for a method that changes the struct?