How to Define Methods on Types in Go

Define Go methods by adding a receiver argument in parentheses before the function name to attach behavior to a type.

Methods bind behavior to types

You are building a geometry library. You define a Circle struct with a Radius field. You write a function func Area(c Circle) float64. It works. Then you add Perimeter, Contains, Scale, and String. Suddenly you have a struct holding data and a dozen functions that all take Circle as their first argument. The logic lives apart from the data. The API feels scattered.

Go lets you glue behavior to data. You define a method. A method is a function that belongs to a type. The syntax signals the relationship. The compiler enforces it. Methods make the API cohesive. They also enable interfaces without explicit declarations.

The receiver is the anchor

A method declaration looks like a function, but the first argument sits in parentheses between func and the method name. This argument is the receiver. The receiver specifies which type the method operates on.

type Circle struct {
    Radius float64
}

// Area calculates the area of the circle.
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

The receiver (c Circle) tells the compiler that Area is a method on Circle. The variable c is just a name for the receiver inside the method body. You access fields using c.Radius. The receiver name follows a convention: use one or two letters matching the type, like c for Circle or u for User. Never use this or self. Go has no hidden receiver. The receiver is an explicit variable.

Methods are functions with a home. Put the logic where the data lives.

Value versus pointer receivers

The receiver can be a value or a pointer. This choice determines whether the method works on a copy or the original data.

type Counter struct {
    count int
}

// Increment adds one to the counter.
// Pointer receiver allows mutation of the underlying struct.
func (c *Counter) Increment() {
    c.count++
}

// Value returns the current count.
// Value receiver copies the struct; safe for read-only access.
func (c Counter) Value() int {
    return c.count
}

func main() {
    var c Counter
    c.Increment()
    fmt.Println(c.Value())
}

Increment uses a pointer receiver *Counter. The method can modify c.count. Value uses a value receiver Counter. The method receives a copy. It cannot change the original struct. The compiler rejects any assignment to fields inside a value receiver method.

Pointer receivers mutate. Value receivers copy. Choose based on intent, not just size.

The compiler rewrites dot notation

When you call a method, you use dot notation: c.Increment(). The compiler rewrites this call internally. If Increment has a pointer receiver, the compiler generates a call to Increment(&c). It takes the address of c and passes it.

This rewrite happens automatically. You don't write &c.Increment(). The compiler handles the address-taking. The rewrite only works if c is addressable. If c is a variable, a struct field, or a slice element, it is addressable. If c is a map value or an interface value, it is not addressable.

The compiler handles the address-taking. You handle the intent.

Addressability rules

Addressability is the gatekeeper for calling pointer methods on values. You can call a pointer method on a value receiver if the value is addressable. The compiler takes the address behind the scenes.

type Buffer struct {
    data []byte
}

// Write appends bytes to the buffer.
// Pointer receiver modifies the slice header.
func (b *Buffer) Write(p []byte) {
    b.data = append(b.data, p...)
}

func main() {
    b := Buffer{}
    // b is addressable. Compiler calls Write(&b).
    b.Write([]byte("hello"))
}

If you try to call a pointer method on a non-addressable value, the compiler rejects the program. Map values are the most common trap. A map value is a copy of the stored data. You cannot take its address.

type Item struct {
    Name string
}

// MarkDone sets a flag on the item.
func (i *Item) MarkDone() {
    i.Name = i.Name + " [done]"
}

func main() {
    m := map[string]Item{
        "task": {Name: "buy milk"},
    }
    // Compiler error: cannot take the address of map value.
    // m["task"].MarkDone()
}

The compiler complains with cannot take the address of map value. You must extract the value, modify it, and store it back. Or store pointers in the map. Map values are copies. You can't mutate a copy through a method.

Methods on built-in types

You cannot define methods on types defined in other packages. This includes built-in types like string, int, and []byte. The compiler rejects the attempt with invalid receiver type string (type defined in other package).

Go does not allow monkey-patching. You must define a new named type based on the built-in type.

// URL wraps a string to add validation methods.
type URL string

// IsValid checks if the URL has a scheme.
func (u URL) IsValid() bool {
    return strings.HasPrefix(string(u), "http")
}

func main() {
    var u URL = "https://example.com"
    fmt.Println(u.IsValid())
}

URL is a distinct type from string. It has its own method set. You can convert between them explicitly. string(u) converts URL to string. URL(s) converts string to URL. The conversion checks at compile time. Wrap the built-in type. Go won't let you monkey-patch string.

Method sets and interfaces

Every type has a method set. The method set determines which interfaces the type satisfies. Go interfaces are satisfied implicitly. If a type has all the methods an interface requires, it satisfies the interface.

The method set of a type T contains all methods with receiver T. The method set of *T contains all methods with receiver T AND all methods with receiver *T.

This asymmetry matters. A pointer satisfies more interfaces than a value.

type Stringer interface {
    String() string
}

type Config struct {
    Host string
}

// String returns the host.
// Pointer receiver means only *Config satisfies Stringer.
func (c *Config) String() string {
    return c.Host
}

func main() {
    var s Stringer
    c := Config{Host: "localhost"}
    // *Config satisfies Stringer.
    s = &c
    // Config does not satisfy Stringer.
    // s = c // compile error
}

If String had a value receiver, both Config and *Config would satisfy Stringer. With a pointer receiver, only *Config does. This rule prevents accidental satisfaction. If a method modifies state, the interface should require a pointer. Points satisfy more interfaces. Check the method set before you pass a value.

Nil receivers

Methods can be called on nil pointers. The receiver is just a variable. If the variable is nil, the method runs with a nil receiver. This is a common idiom for implementing interfaces like Stringer or MarshalJSON.

type User struct {
    Name string
}

// String returns the name or a placeholder.
// Handle nil receiver to avoid panic.
func (u *User) String() string {
    if u == nil {
        return "<nil user>"
    }
    return u.Name
}

func main() {
    var u *User
    fmt.Println(u.String())
}

The method checks u == nil before accessing fields. This prevents panics. The Stringer interface expects String() string. A nil pointer can still satisfy the interface if the method handles nil. Always check for nil in methods that might receive nil pointers.

Pitfalls and compiler errors

Shadowing the receiver is a silent logic bug. If you declare a variable with the same name as the receiver inside the method, you hide the receiver.

type Counter struct {
    count int
}

// Reset sets count to zero.
func (c Counter) Reset() {
    // Shadowing c. This declares a new local variable.
    c := Counter{}
    // c.count is zero, but the original receiver is untouched.
    _ = c.count
}

The compiler allows this. The method compiles. The logic is wrong. The receiver c is never modified. Use a linter to catch shadowing. Or use a distinct name for local variables.

Another pitfall is mixing receiver styles inconsistently. If some methods use pointers and others use values, callers must remember which is which. It creates confusion. The community convention is to pick one receiver style for all methods of a type. If any method needs a pointer, use pointers for all methods. Consistency beats micro-optimization. Pick one receiver style per type.

When to use methods

Methods are the standard way to attach behavior to types in Go. They replace free functions when the logic is tightly coupled to the data. They enable interface satisfaction. They organize code.

Use a value receiver when the method reads fields and the struct is small enough to copy cheaply.

Use a pointer receiver when the method modifies fields.

Use a pointer receiver when the struct is large enough that copying would waste CPU cycles.

Use a pointer receiver when you need the type to satisfy an interface that requires pointer semantics.

Define a new named type when you need methods on a built-in type like string or slice.

Use a single receiver style for all methods of a type to avoid confusion.

Methods are functions with a home. Put the logic where the data lives.

Where to go next