Method Declarations
Learn how to define methods on types in Go, the difference between value and pointer receivers, and how method sets work.
Go does not have classes in the traditional object‑oriented sense, but it gives you a direct way to attach behavior to any type you define. That mechanism is the method declaration. A method is a function that carries an extra receiver argument placed between the func keyword and the function name. The receiver binds the function to a particular type, turning it into a member‑like operation you can call on values of that type.
This document covers the syntax of method declarations, explains the critical distinction between value receivers and pointer receivers, and introduces method sets—the rulebook Go uses to decide which types satisfy an interface.
Method Syntax
A method declaration looks almost identical to a function declaration. The only structural difference is the receiver parameter that appears before the method name.
func (receiver Type) MethodName(parameters) (results) {
// body
}
The receiver can be a struct type, a custom numeric type, a function type, or even a map type, as long as it satisfies four rules: the type must be defined in the same package as the method declaration, it must not be a pointer type itself (though the receiver can be a pointer to the type), it must not be an interface type, and it must be a defined type—you cannot declare a method on a raw built‑in like int directly.
A short example makes the idea concrete.
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
// Abs is a method with a value receiver of type Vertex.
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
Calling v.Abs() prints 5. The receiver v behaves like an implicit first argument: inside Abs, v.X and v.Y refer to the fields of the Vertex the method was called on. This is the mental model to hold onto—the receiver is just a parameter, passed automatically when you use the value.method() syntax.
Methods are functions at heart:
Underneath the syntax, Go treats a method as a regular function with the receiver as its first parameter. The compiler generates an implicit function like Vertex.Abs(v Vertex) float64 and rewrites the method body to call it. You can even invoke that implicit form directly: Vertex.Abs(v). This detail is rarely needed day‑to‑day, but it helps demystify how receivers actually work.
Methods can be defined on non‑struct types as well. The following snippet creates a custom integer type and gives it a double method.
type myInt int
func (m myInt) double() myInt {
return m * 2
}
func main() {
n := myInt(5)
fmt.Println(n.double()) // prints 10
}
Because myInt is defined in the same package as the method, this is allowed. You could not declare a method on int directly because int lives in the built‑in package and you are not writing code there. Custom type definitions solve that.
Methods with the same name can exist on different types, which functions in the same package cannot do. This property is the basis for interfaces.
Method declaration outside the type's package fails:
If you try to define a method on a type that belongs to another package—including built‑ins like int or string—the compiler will reject it with an error like cannot define new methods on non-local type int. Always declare methods in the same package that defines the type. If you need to extend a foreign type, wrap it in a new type or use a free function.
When you see func (e Employee) displaySalary() in a real codebase, the author is simply grouping behaviour that operates on Employee values. This keeps the code readable: everything you can do with an Employee is discoverable right next to the struct definition.
Value Receivers vs Pointer Receivers
The receiver in a method declaration can be a value of type T or a pointer *T. The choice determines whether the method can modify the caller’s original data and how the receiver is passed at runtime.
How each receiver behaves
A value receiver receives a copy of the caller’s value. Changes inside the method affect only that copy, never the original variable.
type Counter struct {
count int
}
// Value receiver: operates on a copy.
func (c Counter) incrementBad() {
c.count++
}
func main() {
c := Counter{count: 0}
c.incrementBad()
fmt.Println(c.count) // prints 0
}
The method increments its own copy of c.count, but the c in main is untouched. This is the same semantics as passing a struct to a function by value.
A pointer receiver gives the method access to the original memory of the caller. Any field assignments or mutations survive the call.
// Pointer receiver: operates on the original Counter.
func (c *Counter) incrementGood() {
c.count++
}
func main() {
c := Counter{count: 0}
c.incrementGood()
fmt.Println(c.count) // prints 1
}
Even though we called c.incrementGood() on a value, Go automatically takes the address of c to satisfy the *Counter receiver. The language makes this transparent: you can call a pointer receiver method on a value, and Go will rewrite c.incrementGood() to (&c).incrementGood(). The reverse also holds—a method with a value receiver can be called on a pointer, and Go dereferences the pointer automatically.
The compiler's courtesy can mask mistakes:
The automatic dereferencing and address‑taking are convenient, but they can hide a design error. If you call a pointer‑receiver method on an unaddressable value (like the return value of a function), the compiler will complain. For example, GetCounter().incrementGood() fails because you cannot take the address of a temporary result. Always be explicit when the mutability of the receiver matters.
When to choose which
The Go community has consolidated around a few straightforward heuristics:
- If the method needs to mutate the receiver’s state, use a pointer receiver.
- If the receiver is a large struct and copying it would be measurably expensive, use a pointer receiver to avoid the copy.
- If the type is a reference type in its nature (a map, slice, or channel), a value receiver usually suffices. Maps and slices already point to shared data, so mutations through them are visible even with a value receiver, though you cannot reassign the map or slice header itself.
- In all other cases, a value receiver is idiomatic. It signals that the method is a pure operation that reads but does not alter the original value.
A common consistency rule is to not mix value and pointer receivers for the same type unless there is a clear reason. If one method on a type needs a pointer receiver, the convention is to make all methods of that type have pointer receivers, keeping the method set predictable.
A consistent receiver choice simplifies interfaces:
If you define some methods of a type with value receivers and others with pointer receivers, the method set of *T will contain all of them, but the method set of T will contain only the value‑receiver methods. This can affect interface satisfaction. Preferring a single receiver type eliminates subtle bugs when you later assign a value to an interface variable.
The copy cost isn't always obvious
For a struct with a few integers, a copy is trivially cheap. For a struct that contains a large array (not a slice—arrays are value types and are copied in full), a value receiver could generate expensive copies. In those cases, a pointer receiver avoids the cost entirely. Measure before you optimize, but be aware of the mechanics.
Misjudging pointer‑receiver necessity is a common beginner mistake:
Beginners often over‑use pointer receivers under the belief that "pointers are faster." For small structs, the copy overhead is negligible, and pointer receivers can prevent escape analysis from allocating the value on the stack, leading to heap allocations that are far more expensive than a copy. Use a pointer receiver when mutation or logical identity is required, not as a premature performance tactic.
Method Sets
Every type in Go has a method set—the complete collection of method signatures that the type (or a pointer to it) owns. Method sets are not something you write; they are computed by the compiler from all the method declarations you provide.
A type’s method set matters because it dictates which interfaces the type satisfies. If an interface requires a method M, a type T satisfies it only if M is in T’s method set. This is the gate for Go’s implicit interface implementation.
Value receiver methods
When you declare a method with a value receiver on type T, that method is added to the method set of T. It is also automatically promoted to the method set of *T. So a value receiver method can be called on both values and pointers.
type Greeter struct {
name string
}
func (g Greeter) Hello() string {
return "Hello, " + g.name
}
// Greeter's method set: { Hello() string }
// *Greeter's method set: { Hello() string }
Pointer receiver methods
When you declare a method with a pointer receiver on type *T, that method is added only to the method set of *T. It is not part of the method set of T.
func (g *Greeter) Rename(newName string) {
g.name = newName
}
// Greeter's method set: { Hello() string } (no Rename)
// *Greeter's method set: { Hello() string, Rename(string) }
This asymmetry has a profound consequence: if an interface requires Rename(string), a plain Greeter value cannot satisfy it; only a *Greeter can. You must pass a pointer to satisfy the interface.
Why the distinction exists
The rule prevents a logical inconsistency. If Go allowed a pointer‑receiver method in the method set of T, you could call it on a value that is not addressable (like a function return). The method might want to mutate the receiver, but there would be no persistent memory to mutate. By restricting pointer‑receiver methods to the pointer’s method set, Go ensures that you always have a modifiable memory location when such a method is invoked.
Method sets are the contract for interface satisfaction:
When you assign a value to an interface variable, Go checks the method set of the value’s type (not the pointer) against the interface at compile time. This is why a *T can satisfy an interface that requires both value‑receiver and pointer‑receiver methods, while a plain T can satisfy only the value‑receiver subset.
Practical impact
Consider an io.Writer interface requiring a Write([]byte) (int, error) method. If you define Write with a pointer receiver on your custom type, you must pass &myVar to any function expecting an io.Writer. Passing myVar will fail to compile.
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) (int, error) {
b.data = append(b.data, p...)
return len(p), nil
}
func process(w io.Writer) { /* ... */ }
func main() {
buf := Buffer{}
// process(buf) // compile error: Buffer does not implement io.Writer
process(&buf) // OK: *Buffer implements io.Writer
}
The error message will mention the method that Buffer is missing, which directly reflects the method set.
Method sets on non‑struct types
Method sets apply identically to any defined type. A custom integer type with a value‑receiver method has that method in its method set and in the method set of its pointer. A pointer‑receiver method on that integer type belongs only to the pointer’s method set. The same rules govern interface satisfaction for numeric types, function types, or map types—though in practice, pointer receivers on small numeric types are rare.
Unaddressable values can block pointer‑receiver calls:
Even with automatic address‑taking, Go cannot take the address of a map element, a function return value, or a constant. Calling a pointer‑receiver method on such a value will fail. For instance, myMap["key"].Rename("x") on a map of Greeter values would not compile because map elements are not addressable. Store a pointer in the map instead: map[string]*Greeter.
Understanding method sets is not an optional detail. It is the mechanism that makes Go’s interface system predictable and safe at compile time. When you design a type, deliberately choosing value vs pointer receivers shapes what interfaces the type can implement—and whether callers need to hand around a pointer or a value.
Summary
Method declarations give Go a lightweight way to associate behavior with data. The receiver parameter defines the type on which the method operates, and the choice between value and pointer receiver controls both mutation semantics and interface eligibility. Value receivers are safe, read‑only operations on a copy; pointer receivers enable state changes and avoid large copies.
Method sets are the compiler‑computed tables of available methods for T and *T. They are the quiet engine behind Go’s implicit interfaces: the method set of a type determines which interfaces it satisfies, and the difference in method sets between T and *T explains why sometimes a pointer is required even though Go automatically takes addresses in simple cases.