CHAPTER 20 · TYPES & ABSTRACTIONS

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
!
Definition vs alias, the equals sign 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

EXERCISE 1

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.