A method in Go is a function associated with a type via a receiver. That receiver indicates which value the method works on.
In the previous article, we saw how to create structs to group data. Now we are going to associate behavior with them through methods.
In languages like Java or C#, methods are defined inside the class (class Person { void Greet() {...} }).
In Go, since there are no classes, methods are declared outside the struct, with a receiver that associates them with the type.
Let’s see how to declare a method and when it is convenient to use a value or pointer receiver.
Anatomy of a method
A method has a receiver section located before its name. The receiver behaves like a parameter within the method body.
Syntax:
func (r Receiver) MethodName() ReturnType { ... }
package main
import "fmt"
type Triangle struct {
Base, Height float64
}
// This is a normal FUNCTION
func AreaFunction(t Triangle) float64 {
return (t.Base * t.Height) / 2
}
// This is a METHOD attached to Triangle
// (t Triangle) is the receiver
func (t Triangle) Area() float64 {
return (t.Base * t.Height) / 2
}
func main() {
myTriangle := Triangle{10, 5}
// Method call (Object-Oriented style)
fmt.Println(myTriangle.Area()) // 25
}The receiver name (t in the example) can be anything, but by convention in Go, the first letter of the type in lowercase is usually used (e.g., t for Triangle, c for Client, etc.). Avoid generic names like this or self.
Value vs pointer receiver
When defining a method, you have two options for the receiver:
- Value receiver:
(t Triangle) - Pointer receiver:
(t *Triangle)
This decision determines which value is copied and which set of methods T and *T implement.
Value receiver
When you use a value receiver, the method receives a COPY of the struct.
Consequences:
- Direct fields: if you assign a field of the receiver, you modify the copy and the original value does not change.
- Referenced data: if a field contains a map, slice, or pointer, its underlying data may still be shared; a value receiver does not guarantee deep immutability.
- Cost: If the struct is very large (e.g., has a 1MB array inside it), it will be copied entirely each time you call the method.
type Counter struct {
Value int
}
// Value Receiver (without *)
func (c Counter) IncrementBad() {
c.Value++ // We increment THE COPY
fmt.Println("Inside method:", c.Value)
}
func main() {
c := Counter{0}
c.IncrementBad() // Prints: Inside: 1
fmt.Println("Outside (Original):", c.Value) // Prints: 0 (Hasn't changed!)
}Pointer receiver
When you put the asterisk *, the method receives a pointer to the original instance.
Consequences:
- Mutability: Any change you make will affect the original variable.
- Copy: the address is copied instead of the entire struct, although this by itself does not guarantee better performance.
// Pointer Receiver (with *)
func (c *Counter) IncrementGood() {
c.Value++ // Go automatically dereferences and modifies the original
}
func main() {
c := Counter{0}
c.IncrementGood()
fmt.Println("Original:", c.Value) // Prints: 1 (Now it works!)
}The convenience of automatic calling
You might think: “If the method expects a pointer, do I have to call it using (&c).Increment()?”
Fortunately, no. Go is very clever.
When calling a pointer method on an addressable variable, Go automatically takes its address. It also allows calling value methods through a pointer.
c := Counter{0} // c is a Value
c.IncrementGood() // Go translates this to (&c).IncrementGood()
p := &Counter{0} // p is a Pointer
p.IncrementBad() // Go translates this to (*p).IncrementBad()This convenience has a limit: a non-addressable temporary value, like Counter{}, cannot directly call a method whose receiver is *Counter.
How to choose the receiver
This decision tree is quite helpful:
Does the method need to modify the object’s state?
- YES ➔ Use Pointer
(p *Person). - NO ➔ Keep reading.
Is the struct large or does it contain a field that must not be copied, like sync.Mutex?
- YES ➔ Use Pointer
(p *Person)to avoid copying everything. - NO ➔ You can use Value.
Consistency?
- If ANY method of the type needs to be a pointer, it is recommended to make ALL of them pointers. This avoids strange mixes and makes the type’s interface predictable.
:::
For small types with value semantics, like coordinates or durations, a value receiver is usually natural. To mutate the receiver, avoid large copies, or protect types that must not be copied, use a pointer. Do not choose pointer just out of performance intuition: measure if it’s relevant.
Receivers on non-struct types
You can define methods on a type defined in the same package, as long as the base type is not a pointer or an interface.
This allows very powerful things, like adding logic to a simple float or a function.
// We define our own type based on float64
type Money float64
// We add a method to format it
func (m Money) String() string {
return fmt.Sprintf("%.2f €", m)
}
func main() {
var price Money = 25.5
fmt.Println(price.String()) // "25.50 €"
}