Methods & Receivers
A method is a function with a special receiver argument, attaching behavior to a named type. Go has no classes; methods are how you give types behavior. This chapter also covers type definitions and type aliases, which are how you create the named types you'll attach methods to.
Learning objectives
- Define methods on a named type.
- Choose between value and pointer receivers.
- Understand method sets and why they matter for interfaces.
- Distinguish a type definition from a type alias.
- Use embedding to compose methods.
Defining a method
type Person struct{ Name string }
func (p Person) Greet() string {
return "Hi, I'm " + p.Name
}
func main() {
p := Person{Name: "Alice"}
fmt.Println(p.Greet()) // Hi, I'm Alice
}
The (p Person) in front of the name is the
receiver. It's like an extra argument the method
operates on.
Value vs pointer receivers
type Counter struct{ n int }
func (c Counter) IncByValue() { c.n++ } // mutates a COPY
func (c *Counter) Inc() { c.n++ } // mutates the original
func main() {
c := Counter{}
c.IncByValue()
fmt.Println(c.n) // 0 , the copy was incremented, not c
c.Inc()
fmt.Println(c.n) // 1 : pointer receiver mutated c
}
Notice you call c.Inc(), not (&c).Inc().
Go automatically takes the address of an addressable value when calling
a pointer-receiver method.
Method sets (matters for interfaces)
- Type
T's method set: only methods with receiver(T). - Type
*T's method set: both(T)and(*T)methods.
Practical impact: if (*T) Foo() is required to satisfy
an interface, only a *T value (not a T value)
satisfies it.
type Stringer interface { String() string }
type T struct{}
func (t *T) String() string { return "T" }
var _ Stringer = &T{} // ✓, *T satisfies Stringer
// var _ Stringer = T{} // ❌: T does NOT (only *T's method set has String)
Named types (type definitions)
type X Y creates a new, distinct type whose
underlying type is Y. You attach methods to the new name.
type Celsius float64
type Fahrenheit float64
func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
c := Celsius(100)
fmt.Println(c.ToFahrenheit()) // 212
Even though Celsius and Fahrenheit have
the same underlying type, the compiler won't let you mix them, the
type system prevents accidental temperature confusion.
var x Celsius = 100
var y Fahrenheit = x // ❌ compile error
var y Fahrenheit = Fahrenheit(x) // ✓ explicit conversion
Type aliases
type X = Y (note the =) is an
alias: X and Y are the same type, just two
names. No methods can be attached. Used mostly for refactoring across
packages or compatibility shims.
type byte = uint8 // built-in alias
type rune = int32 // built-in alias
type X Y = new type. type X = Y = alias.
One character, very different behavior.
Methods on non-struct types
type Words []string
func (w Words) JoinComma() string {
return strings.Join(w, ", ")
}
w := Words{"Go", "Rust", "Zig"}
fmt.Println(w.JoinComma()) // Go, Rust, Zig
Methods can be attached to any type defined in the same package. You can't attach methods to types from other packages, define your own type wrapping it first.
Methods through embedding
Embed a type and its methods are promoted to the outer type:
type Animal struct{ Name string }
func (a Animal) Speak() string { return "Hi, I'm " + a.Name }
type Dog struct {
Animal
Breed string
}
d := Dog{Animal: Animal{Name: "Buddy"}, Breed: "Husky"}
fmt.Println(d.Speak()) // Hi, I'm Buddy, promoted from Animal
This is how Go does code reuse without inheritance. Dog
"has-a" Animal (composition), not "is-a".
Picking a receiver, decision guide
- Pointer receiver if: the method modifies the receiver, or the type contains a sync primitive (
sync.Mutex) that mustn't be copied, or the type is large. - Value receiver if: the type is small (basic types, small structs) and the method only reads.
- Be consistent: if any method on a type has a pointer receiver, give them all pointer receivers. Mixing is confusing.
Check your understanding
Practice exercises
Add methods to Money
Define type Money int (representing cents). Add methods Dollars() float64 (returns dollars), Add(other Money) Money, and String() string (returns e.g. "$12.34"). Verify that fmt.Println(Money(1234)) prints $12.34.
Show solution
type Money int // value in cents
func (m Money) Dollars() float64 { return float64(m) / 100 }
func (m Money) Add(o Money) Money { return m + o }
func (m Money) String() string { return fmt.Sprintf("$%.2f", m.Dollars()) }
Defining a String() string method means fmt uses it automatically (this is the Stringer interface, Chapter 21).
Further reading
Behavior on types, done.