Method Receivers in Go: Demystifying Value and Pointer Receivers

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy. Go doesn't have traditional classes or inheritance, but it does let you associate methods with types. This happens through method receivers. If you've ever wondered whether to use a value or a pointer receiver in Go, this is the detailed, no-nonsense guide for you. What Are Method Receivers in Go? A method receiver in Go is the value or pointer on which a method operates. It is defined like so: func (r ReceiverType) MethodName(...) ReturnType { // implementation } This makes MethodName a method tied to ReceiverType. Go doesn't have classes, but this is how you associate behavior with data. Unlike Java or C++, Go makes the receiver explicit, and it can either be a value or a pointer. Two Kinds of Receivers: Value and Pointer Go supports two types of method receivers: Receiver Type Syntax Works On Allows Mutation Allocates on Heap Value (t T) Copy No No Pointer (t *T) Original Yes Maybe Value Receivers: Call by Value Syntax func (u User) Greet() string { return "Hello, " + u.name } Behavior Copies the struct when the method is called. Cannot modify the original struct. Preferred for small, immutable-like types. Example type User struct { name string } func (u User) Rename(newName string) { u.name = newName // modifies copy } func main() { user := User{name: "Alice"} user.Rename("Bob") fmt.Println(user.name) // still "Alice" } When to Use The struct is small. You don't need to modify fields. You want consistent behavior (immutability-style). More details: Effective Go: Methods Pointer Receivers: Reference-Like Semantics Syntax func (u *User) Rename(newName string) { u.name = newName } Behavior Operates on the original struct. Can modify the receiver's internal state. Can be more efficient for large structs. Example type User struct { name string } func (u *User) Rename(newName string) { u.name = newName } func main() { user := User{name: "Alice"} user.Rename("Bob") fmt.Println(user.name) // "Bob" } When to Use You want to modify the receiver. The struct is large or contains reference-heavy fields. You already use pointer receivers elsewhere on the type. More details: Go FAQ: Pointers Method Sets and Their Role Go defines what methods a type has through method sets: For T: includes all methods with value receivers. For *T: includes all methods with pointer or value receivers. This impacts whether a type can satisfy an interface. Example type Stringer interface { String() string } type Data struct { value int } func (d *Data) String() string { return fmt.Sprintf("%d", d.value) } func main() { var s Stringer d := Data{42} // s = d // Compile error: Data does not implement Stringer s = &d // OK: *Data has String method fmt.Println(s.String()) } How Go Handles Calls: Implicit Conversions Go is flexible when it comes to calling methods: Method Defined On Can Be Called With T T or *T *T *T only type Counter struct { count int } func (c Counter) Print() {...} // Can call with value or pointer func (c *Counter) Increment() {...} // Only with pointer When calling value.Increment(), Go will auto-take the address if needed — but only if the method has a pointer receiver. Real Performance Implications Value Receiver type Big struct { data [1024]int } func (b Big) Process() { // copies 8KB } Pointer Receiver type Big struct { data [1024]int } func (b *Big) Process() { // avoids copy } Using a value receiver here would trigger a full copy on each method call. Prefer pointer receivers when working with large data types or when mutation is required. Read more: Go Memory Model Consistency Rule: Avoid Mixing Receiver Types If one method on a type uses a pointer receiver, all methods should. This avoids: Confusion about behavior. Interface method mismatch. Implicit address/dereference bugs. type Confused struct { name string } func (c Confused) GetName() string { return c.name } func (c *Confused) SetName(n string) { c.name = n } Better: type Clear struct { name string } func (c *Clear) GetName() string { return c.name } func (c *Clear) SetName(n string) { c.name = n } Receiver Behavior with Maps, Slices, Channels Value receivers do not protect against mutation if fields themselves are reference types: type Data struct { values []int } func (d Data) Add(val int) { d.values = append(d.values, val) // modifies shared slice }

Apr 17, 2025 - 18:55
 0
Method Receivers in Go: Demystifying Value and Pointer Receivers

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy.

Go doesn't have traditional classes or inheritance, but it does let you associate methods with types.

This happens through method receivers.

If you've ever wondered whether to use a value or a pointer receiver in Go, this is the detailed, no-nonsense guide for you.

What Are Method Receivers in Go?

A method receiver in Go is the value or pointer on which a method operates. It is defined like so:

func (r ReceiverType) MethodName(...) ReturnType {
    // implementation
}

This makes MethodName a method tied to ReceiverType. Go doesn't have classes, but this is how you associate behavior with data.

Unlike Java or C++, Go makes the receiver explicit, and it can either be a value or a pointer.

Two Kinds of Receivers: Value and Pointer

Go supports two types of method receivers:

Receiver Type Syntax Works On Allows Mutation Allocates on Heap
Value (t T) Copy No No
Pointer (t *T) Original Yes Maybe

Value Receivers: Call by Value

Syntax

func (u User) Greet() string {
    return "Hello, " + u.name
}

Behavior

  • Copies the struct when the method is called.
  • Cannot modify the original struct.
  • Preferred for small, immutable-like types.

Example

type User struct {
    name string
}

func (u User) Rename(newName string) {
    u.name = newName // modifies copy
}

func main() {
    user := User{name: "Alice"}
    user.Rename("Bob")
    fmt.Println(user.name) // still "Alice"
}

When to Use

  • The struct is small.
  • You don't need to modify fields.
  • You want consistent behavior (immutability-style).

More details: Effective Go: Methods

Pointer Receivers: Reference-Like Semantics

Syntax

func (u *User) Rename(newName string) {
    u.name = newName
}

Behavior

  • Operates on the original struct.
  • Can modify the receiver's internal state.
  • Can be more efficient for large structs.

Example

type User struct {
    name string
}

func (u *User) Rename(newName string) {
    u.name = newName
}

func main() {
    user := User{name: "Alice"}
    user.Rename("Bob")
    fmt.Println(user.name) // "Bob"
}

When to Use

  • You want to modify the receiver.
  • The struct is large or contains reference-heavy fields.
  • You already use pointer receivers elsewhere on the type.

More details: Go FAQ: Pointers

Method Sets and Their Role

Go defines what methods a type has through method sets:

  • For T: includes all methods with value receivers.
  • For *T: includes all methods with pointer or value receivers.

This impacts whether a type can satisfy an interface.

Example

type Stringer interface {
    String() string
}

type Data struct {
    value int
}

func (d *Data) String() string {
    return fmt.Sprintf("%d", d.value)
}

func main() {
    var s Stringer

    d := Data{42}
    // s = d      // Compile error: Data does not implement Stringer
    s = &d        // OK: *Data has String method
    fmt.Println(s.String())
}

How Go Handles Calls: Implicit Conversions

Go is flexible when it comes to calling methods:

Method Defined On Can Be Called With
T T or *T
*T *T only
type Counter struct {
    count int
}

func (c Counter) Print() {...}       // Can call with value or pointer
func (c *Counter) Increment() {...}  // Only with pointer

When calling value.Increment(), Go will auto-take the address if needed — but only if the method has a pointer receiver.

Real Performance Implications

Value Receiver

type Big struct {
    data [1024]int
}

func (b Big) Process() {
    // copies 8KB
}

Pointer Receiver

type Big struct {
    data [1024]int
}

func (b *Big) Process() {
    // avoids copy
}

Using a value receiver here would trigger a full copy on each method call. Prefer pointer receivers when working with large data types or when mutation is required.

Read more: Go Memory Model

Consistency Rule: Avoid Mixing Receiver Types

If one method on a type uses a pointer receiver, all methods should. This avoids:

  • Confusion about behavior.
  • Interface method mismatch.
  • Implicit address/dereference bugs.
type Confused struct {
    name string
}

func (c Confused) GetName() string {
    return c.name
}

func (c *Confused) SetName(n string) {
    c.name = n
}

Better:

type Clear struct {
    name string
}

func (c *Clear) GetName() string {
    return c.name
}

func (c *Clear) SetName(n string) {
    c.name = n
}

Receiver Behavior with Maps, Slices, Channels

Value receivers do not protect against mutation if fields themselves are reference types:

type Data struct {
    values []int
}

func (d Data) Add(val int) {
    d.values = append(d.values, val) // modifies shared slice
}

Even though d is a value receiver, the underlying slice may be shared and mutated.

Interface Compatibility and Traps

Scenario

You define an interface that expects a method with pointer receiver:

type Writer interface {
    WriteData() error
}

type File struct {}

func (f *File) WriteData() error {...}
var w Writer
var f File
w = &f // valid
w = f  // invalid: File does not implement Writer

You must use the pointer if the method is defined on *T.

Summary: Choosing the Right Receiver

Criteria Value Receiver Pointer Receiver
Needs to modify receiver? ❌ No ✅ Yes
Struct is large? ❌ Avoid ✅ Prefer
All methods should match? ❌ Risky ✅ Safer
Avoid heap allocation? ✅ Usually ❌ Maybe escapes

Rule of Thumb

  • If any method needs a pointer receiver, make all methods use pointer receivers.
  • Keep behavior and memory usage predictable.

For More Reading

Got questions or deeper edge cases? Leave a comment. This topic is subtle but critical to mastering Go.