Start learning free

LESSON 153 OF 338 · GO ESSENTIALS

Methods and Receivers in Go: Value vs Pointer Receivers

Lesson Read the full explanation

Go has no classes. Instead you bundle data into a struct — a group of named fields — and attach behavior to it from the outside, by writing functions with a receiver:

type Counter struct {
    count int
}

func (c Counter) Inc() {   // "(c Counter)" is the receiver
    c.count++
}

The extra (c Counter) before the name turns a plain function into a method: you call it with dot syntax, c.Inc(), and inside the method c refers to the counter it was called on. That's the whole Go replacement for classes — struct for data, methods for behavior.

But there are TWO kinds of receiver, and picking the wrong one is the most famous silent bug in Go.


The trap: a value receiver gets a COPY.

With (c Counter) — a value receiver — the method receives a copy of your struct. It increments the copy, the copy is thrown away, and your counter never changes. No error, no warning:

c := Counter{count: 0}
c.Inc()
fmt.Println(c.count)   // prints 0 — the ++ happened on a copy!

Write the receiver as a pointer instead — (c *Counter) — and the method receives the address of the ORIGINAL struct, so the mutation sticks. Calling it is no extra work: for a variable, Go automatically takes &c for you.

One thing Go will NOT do is take the address of a throwaway value. Call a pointer method directly on a struct literal and the compiler stops you:

Counter{count: 0}.IncP()   // a literal has no address
./main.go:14:23: cannot call pointer method IncP on Counter

Rule of thumb: if the method needs to CHANGE the struct, the receiver must be *T. If it only reads, a value receiver is fine.


Assignment
Follow the Coding Tutorial and catch the bug in the act:

  • define Counter, a struct with one int field count,
  • attach Inc() with a value receiver (c Counter) that does c.count++,
  • attach IncP() with a pointer receiver (c *Counter) with the same body,
  • in mainFunction, create c := Counter{count: 0}, call c.Inc() and print the count — it is still 0,
  • call c.IncP() and print again — now it is 1.

The exact expected output is:

after Inc(): count = 0
after IncP(): count = 1

Hint
The two methods differ by a single character in the receiver: (c Counter) vs (c *Counter). That character decides whether the increment survives.


STEP 1 OF 9
    Ctrl + Enter to check

    GO · GO ESSENTIALS

    Methods And Receivers in Go, explained

    Structs and methods instead of classes

    Go has no classes. Data lives in a struct, a group of named fields, and behavior is attached from the outside with functions that have a receiver. A function with a receiver is called a method.

    type Counter struct {
        count int
    }
    
    func (c Counter) Value() int {
        return c.count
    }
    

    The part in parentheses before the name, (c Counter), is the receiver. It works like a first parameter that is written in front of the function name. You call the method with dot syntax, c.Value(), and inside the method c is the counter it was called on. Methods can be declared for any type you define in your own package, not only structs. They must be declared at the top level of the file.

    Two kinds of receiver

    The receiver can be written two ways, and they behave differently:

    • Value receiver, (c Counter). The method gets a copy of the struct. Changes to its fields affect only the copy, which disappears when the method returns.
    • Pointer receiver, (c *Counter). The method gets the address of the original struct, so changes to its fields are visible to the caller.

    This is a classic silent Go bug: a method that is meant to change a struct but has a value receiver compiles, runs without errors, and changes nothing.

    The exercise, step by step

    The exercise lets you see the difference with two almost identical methods.

    1. Define the data. Counter is a struct with one int field, count. At this point it has no behavior.
    2. Add Inc with a value receiver. Its body is c.count++. The receiver c is a copy, so the increment lands on the copy.
    3. Add IncP with a pointer receiver. The only difference in the header is the * in front of Counter.
    4. Give it the same body. Through a pointer, Go reaches the field automatically, so you write c.count++, not (*c).count++. Keeping the body identical means the receiver is the only thing that differs between the two methods.
    5. Write mainFunction. Methods are declared at the top level, so the experiment needs a function to run in.
    6. Create the counter. c := Counter{count: 0} is a struct literal that names each field.
    7. Call Inc, then print. The count is still 0.
    8. Call IncP, then print again. The count is now 1. The call looks the same as before. Because c is a variable, Go takes its address for you and passes &c.
    type Counter struct {
        count int
    }
    
    func (c Counter) Inc() {
        c.count++
    }
    
    func (c *Counter) IncP() {
        c.count++
    }
    
    func mainFunction() {
        c := Counter{count: 0}
        c.Inc()
        fmt.Println("after Inc(): count =", c.count)
        c.IncP()
        fmt.Println("after IncP(): count =", c.count)
    }
    

    The program prints:

    after Inc(): count = 0
    after IncP(): count = 1
    

    fmt.Println separates its arguments with a space, which is why the label ends in = with no trailing space of its own.

    Go details and gotchas

    • Calls through variables and pointers both work. If p is a *Counter, both p.Inc() and p.IncP() compile. Go dereferences for the value method, which then works on a copy, exactly as before.
    • Pointer methods need an addressable value. A variable has an address, so c.IncP() works. A struct literal has none. This line does not compile:
    Counter{count: 0}.IncP()
    
    cannot call pointer method IncP on Counter
    
    • Loops hand you copies. In for _, k := range counters over a slice of Counter values, k is a copy of each element, so k.IncP() changes the copy only and the slice stays as it was. Index into the slice instead: counters[i].IncP() changes the element itself.
    • Map values are not addressable. You cannot call a pointer method on m["a"] directly. Copy the value to a variable, call the method, and store it back.
    • Nil pointers. A *Counter that is nil can call a method, but touching a field inside it stops the program with a nil pointer dereference panic. With a value receiver, the call itself panics, because Go has to copy a struct that doesn't exist.
    • Interfaces see the difference. If an interface requires IncP(), a Counter value does not satisfy it, but a *Counter does. The compiler says the method has a pointer receiver.
    • Be consistent. If any method of a type needs a pointer receiver, it is conventional to use pointer receivers for all of its methods.

    When to use which

    If the method changes the struct, the receiver must be *T. If the struct is large, a pointer also avoids copying it on every call. If the method only reads a small struct, a value receiver is fine.

    Key takeaways

    • A struct holds the data, and methods with a receiver attach behavior to it.
    • A value receiver works on a copy, so its changes vanish silently.
    • A pointer receiver, *T, works on the original, so its changes stick.
    • Go takes the address for you when you call a pointer method on a variable, but not on a literal or a map element.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free