25. Methods

📖 Reading · 10 min
💡 Every code box below is live — edit it and hit Run.

A method is a function with a receiver — a value it's attached to. That's the whole idea. There's no class body to put it in; a method is declared at package level like any function, with one extra parameter in front of the name.

Declaring a method

package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func main() {
    r := Rectangle{Width: 3, Height: 4}

    fmt.Println(r.Area())
    fmt.Println(r.Perimeter())
}

The (r Rectangle) between func and the name is the receiver. Inside the method, r is an ordinary parameter holding the value you called it on.

By convention the receiver name is short — one or two letters, usually the first letter of the type — and it's the same name on every method of that type. Go programmers don't use this or self.

A method is a function with different syntax

package main

import "fmt"

type Celsius float64

func (c Celsius) ToF() float64 {
    return float64(c)*9/5 + 32
}

func toF(c Celsius) float64 {
    return float64(c)*9/5 + 32
}

func main() {
    temp := Celsius(100)

    fmt.Println(temp.ToF())
    fmt.Println(toF(temp))
}

Identical work, two spellings. Methods win because they're discoverable (temp. shows you everything a Celsius can do), they let different types share a method name, and — the big one — they're how a type satisfies an interface.

Methods on any named type, not just structs

That example did something worth pausing on: Celsius is not a struct, it's a named float64. You can define methods on any type you declare in your package:

package main

import (
    "fmt"
    "strings"
)

type Tags []string

func (t Tags) Contains(want string) bool {
    for _, tag := range t {
        if tag == want {
            return true
        }
    }
    return false
}

func (t Tags) String() string {
    return "[" + strings.Join(t, ", ") + "]"
}

type WordCount map[string]int

func (w WordCount) Total() int {
    sum := 0
    for _, n := range w {
        sum += n
    }
    return sum
}

func main() {
    t := Tags{"go", "backend", "wasm"}
    fmt.Println(t.Contains("go"), t.Contains("rust"))
    fmt.Println(t.String())

    wc := WordCount{"a": 3, "b": 5}
    fmt.Println(wc.Total())
}

Naming a slice or map type and hanging methods off it is very idiomatic Go — it turns a bag of data into something with vocabulary.

The one restriction: you can only define methods on types declared in your own package. You can't add a method to string or to time.Time. If you need to, declare type MyTime time.Time and add methods to that.

String() — the method fmt looks for

Give your type a String() string method and every fmt function will use it automatically:

package main

import "fmt"

type Point struct {
    X, Y int
}

func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

func main() {
    p := Point{3, 4}

    fmt.Println(p)
    fmt.Printf("%v and %s\n", p, p)
    fmt.Println([]Point{{1, 2}, {3, 4}})
}

Without it you'd see {3 4}. With it, your type controls how it prints everywhere — including inside slices and maps.

This is your first interface, whether you noticed or not: fmt.Stringer is defined as "any type with a String() string method", and Point satisfies it just by having one. Nothing was declared, imported or registered. The next module is entirely about that mechanism.

One warning: never call fmt.Sprintf("%v", p) on the receiver inside its own String method — %v will call String again, forever. Format the fields, not the whole value.

Methods and the value copy rule

A value receiver gets a copy, exactly like a normal parameter:

package main

import "fmt"

type Counter struct {
    N int
}

func (c Counter) IncrementBroken() {
    c.N++
}

func main() {
    c := Counter{}
    c.IncrementBroken()
    c.IncrementBroken()
    fmt.Println("after two increments:", c.N)
}

Still 0. IncrementBroken incremented its own copy and discarded it.

This is the most common surprise in the whole language for newcomers, and it's the entire subject of the next lesson: to modify the receiver, you need a pointer receiver.

Method values

Because a method is a function, you can grab one as a value:

package main

import "fmt"

type Greeter struct {
    Name string
}

func (g Greeter) Hello() string {
    return "Hello, " + g.Name
}

func main() {
    g := Greeter{Name: "Ada"}

    f := g.Hello
    fmt.Println(f())

    g.Name = "Grace"
    fmt.Println(f())
    fmt.Println(g.Hello())
}

f := g.Hello captures the receiver too — a copy of g as it was at that moment. Changing g afterwards doesn't affect f. That's the value-copy rule again, showing up in a new place.

Your turn

Give the Circle type an Area() method (π r², use math.Pi) and a Describe() method that returns a string. The program should print exactly:

78.54
circle with radius 5.0
package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

// add Area() float64 and Describe() string

func main() {
    c := Circle{Radius: 5}
    fmt.Printf("%.2f\n", c.Area())
    fmt.Println(c.Describe())
}
package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Describe() string {
    return fmt.Sprintf("circle with radius %.1f", c.Radius)
}

func main() {
    c := Circle{Radius: 5}
    fmt.Printf("%.2f\n", c.Area())
    fmt.Println(c.Describe())
}

Next: the other kind of receiver, and the one rule you must not break.