A method is a function attached to a type. Once you attach it, you call it with the dot operator on values of that type. If you’ve used object-oriented languages, this is similar to “instance methods” — but in Go it works without any class system.

Defining a method

A method is a function with one extra piece: a receiver, written between func and the function name.

package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

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

func main() {
    r := Rectangle{Width: 5, Height: 3}
    fmt.Println("Area:", r.Area())
}
Area: 15

The receiver (r Rectangle) says: “this is a method on the Rectangle type, and inside the method we’ll refer to the value as r.” It’s just a parameter, declared in a special spot.

You can name the receiver anything, but the convention is a short letter that hints at the type — r for Rectangle, u for User, s for Server. Keep it short.

Why methods?

Couldn’t we just write a regular function Area(r Rectangle) float64? Technically yes. The advantages of using a method:

  • Discoverability. Editors offer methods as autocompletions on the type. r. shows everything you can do with a Rectangle.
  • Grouping. All the behavior of a type lives next to the type. The reader doesn’t have to hunt.
  • Interfaces. Methods are the only way to satisfy interfaces — and interfaces are how Go does flexible, polymorphic code (next lesson).

Pointer receivers

There are two kinds of receivers: value receivers and pointer receivers.

A value receiver gets a copy of the value. Changes inside the method don’t affect the caller’s value. A pointer receiver gets a pointer to the original — changes do affect the caller.

package main

import "fmt"

type Counter struct {
    count int
}

func (c Counter) IncrementCopy() {
    c.count++
}

func (c *Counter) Increment() {
    c.count++
}

func main() {
    c := Counter{}

    c.IncrementCopy()
    c.IncrementCopy()
    fmt.Println("After IncrementCopy:", c.count)

    c.Increment()
    c.Increment()
    fmt.Println("After Increment:", c.count)
}
After IncrementCopy: 0
After Increment: 2

IncrementCopy modified a local copy that gets thrown away. Increment had a pointer to the actual Counter, so the change stuck.

Almost all real Go code uses pointer receivers. The general rule: if a method modifies the receiver, must be a pointer receiver. If the type is small and the method doesn’t modify, value receiver is fine. When in doubt, use a pointer.

Calling methods on pointers — Go’s helpful trick

In the example above, notice we wrote c.Increment() even though Increment has a pointer receiver. Go automatically takes the address of c for us. We could have written (&c).Increment() but no one does.

This works going the other way too — you can call a value-receiver method on a pointer, and Go automatically dereferences.

The takeaway: just write c.Method() and don’t worry about the conversion.

Methods on any named type

Methods aren’t limited to structs. You can attach methods to any type you define in your code, including ones based on built-in types.

package main

import "fmt"

type Celsius float64

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

func main() {
    temp := Celsius(25)
    fmt.Println(temp, "°C is", temp.ToFahrenheit(), "°F")
}
25 °C is 77 °F

This is one of Go’s quietly powerful patterns. Wrap a primitive type with a meaningful name and attach behavior to it.

You can’t attach methods to types from other packages — only to types you define in your own package. So you can’t add a method to the built-in int, but you can add one to your own type UserID int.

A complete example

A small bank account, with deposits and withdrawals:

package main

import "fmt"

type Account struct {
    Owner   string
    Balance float64
}

func (a *Account) Deposit(amount float64) {
    a.Balance += amount
}

func (a *Account) Withdraw(amount float64) bool {
    if amount > a.Balance {
        return false
    }
    a.Balance -= amount
    return true
}

func (a Account) Statement() string {
    return fmt.Sprintf("%s has $%.2f", a.Owner, a.Balance)
}

func main() {
    acc := &Account{Owner: "Mani", Balance: 1000}

    acc.Deposit(500)
    fmt.Println(acc.Statement())

    if ok := acc.Withdraw(2000); !ok {
        fmt.Println("Withdrawal failed: insufficient funds")
    }

    acc.Withdraw(300)
    fmt.Println(acc.Statement())
}
Mani has $1500.00
Withdrawal failed: insufficient funds
Mani has $1200.00

Deposit and Withdraw modify the account, so they have pointer receivers. Statement doesn’t modify anything, so it’s fine as a value receiver.

Go style says: be consistent. If any method on a type uses a pointer receiver, all of them should. Mixing receiver styles on the same type confuses readers and can cause subtle issues with interfaces (which we’re about to cover).

What’s next

You can group data with structs and attach behavior with methods. The final piece — and arguably the most powerful Go feature — is interfaces: writing code that works with any type that has certain methods. That’s next.

Toggle theme (T)