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.
- Define the data.
Counteris a struct with oneintfield,count. At this point it has no behavior. - Add
Incwith a value receiver. Its body isc.count++. The receivercis a copy, so the increment lands on the copy. - Add
IncPwith a pointer receiver. The only difference in the header is the*in front ofCounter. - 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. - Write
mainFunction. Methods are declared at the top level, so the experiment needs a function to run in. - Create the counter.
c := Counter{count: 0}is a struct literal that names each field. - Call
Inc, then print. The count is still 0. - Call
IncP, then print again. The count is now 1. The call looks the same as before. Becausecis 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
pis a*Counter, bothp.Inc()andp.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 countersover a slice ofCountervalues,kis a copy of each element, sok.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
*Counterthat isnilcan 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(), aCountervalue does not satisfy it, but a*Counterdoes. 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.