Skip to main content

Methods and Receivers in Go

Published: May 8, 2026 Updated: August 29, 2026 Larry Qu 6 min read

A method in Go is a function with a receiver — an extra parameter that appears before the function name. The receiver specifies which type the method belongs to. Methods let you attach behavior to any named type, not just structs, and they’re how Go implements the behavior side of its interface system.

The choice between a value receiver and a pointer receiver has real consequences for correctness, performance, and interface satisfaction. It’s not just a style choice.

For struct definition and embedding see Go structs composition and embedding. For interfaces see Go interfaces.

Value Receivers

A value receiver receives a copy of the value. The method operates on that copy — any changes to the receiver inside the method don’t affect the original:

type Circle struct {
    Radius float64
}

// Value receiver: Area gets a copy of c
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

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

// String satisfies fmt.Stringer — conventionally a value receiver
func (c Circle) String() string {
    return fmt.Sprintf("Circle(r=%.2f)", c.Radius)
}

Value receivers are appropriate for:

  • Small structs where copying is cheap (a few fields of basic types)
  • Read-only operations that don’t need to modify the receiver
  • Types that should behave like values — structs that are semantically equivalent to their field values, like time.Time or net.IP

Pointer Receivers

A pointer receiver receives a pointer to the value. The method can modify the original:

type Stack struct {
    items []int
}

// Pointer receiver: Push modifies the original Stack
func (s *Stack) Push(item int) {
    s.items = append(s.items, item)
}

func (s *Stack) Pop() (int, bool) {
    if len(s.items) == 0 {
        return 0, false
    }
    n := len(s.items)
    item := s.items[n-1]
    s.items = s.items[:n-1]
    return item, true
}

func (s *Stack) Len() int {
    return len(s.items)
}

Pointer receivers are appropriate for:

  • Mutations — any method that changes the receiver’s state
  • Large structs where copying would be expensive
  • Types with mutable state — anything with internal counters, buffers, or connections

The Consistency Rule

If any method on a type uses a pointer receiver, all methods should use pointer receivers. Mixing them on the same type creates confusion about method sets and interface satisfaction:

// ❌ Inconsistent — some value, some pointer
type User struct{ Name string }
func (u User)  GetName() string  { return u.Name }      // value
func (u *User) SetName(s string) { u.Name = s }          // pointer
func (u User)  String() string   { return u.Name }       // value

// ✅ Consistent — all pointer
type User struct{ Name string }
func (u *User) GetName() string  { return u.Name }
func (u *User) SetName(s string) { u.Name = s }
func (u *User) String() string   { return u.Name }

The exception is String() string for fmt.Stringer and Error() string for error — these are conventionally value receivers because you usually want both User and *User to print nicely. But if all other methods use pointer receivers, use pointer for String() too.

Method Sets and Interface Satisfaction

This is where value vs pointer receivers has real impact. Go’s method sets:

  • A value of type T has the method set of all methods with value receiver (t T)
  • A pointer of type *T has the method set of all methods with either value or pointer receiver

In plain English: a pointer can call both kinds of methods, but a value can only call value-receiver methods.

This matters for interface satisfaction:

type Writer interface {
    Write(data string) error
}

type FileWriter struct{ path string }

func (fw *FileWriter) Write(data string) error {
    // ... write to file ...
    return nil
}

var w Writer = &FileWriter{path: "out.txt"}  // ✅ *FileWriter satisfies Writer
var w Writer = FileWriter{path: "out.txt"}   // ❌ compile error: FileWriter does not implement Writer

FileWriter (value) doesn’t satisfy Writer because Write has a pointer receiver. Only *FileWriter satisfies it. The fix is almost always to use &FileWriter{} — and that’s fine, you should be using pointers for mutable types anyway.

A compile-time assertion makes this contract explicit:

var _ Writer = (*FileWriter)(nil)  // fails to compile if *FileWriter doesn't implement Writer

Go’s Automatic Address-Taking

When you call a pointer-receiver method on an addressable value, Go automatically takes its address:

s := Stack{}      // value, not pointer
s.Push(42)        // Go rewrites this as (&s).Push(42) — works fine

This convenience only works for addressable values (variables, struct fields, array elements). It doesn’t work for non-addressable values like function return values or map elements:

// ❌ Cannot take address of map[string]Stack{}["key"]
m := map[string]Stack{}
m["key"].Push(42)  // compile error

// ✅ Use a pointer in the map
m := map[string]*Stack{}
m["key"] = &Stack{}
m["key"].Push(42)

Method Chaining

Returning the receiver from a pointer-receiver method enables fluent chaining:

type QueryBuilder struct {
    table  string
    wheres []string
    limit  int
    offset int
}

func (q *QueryBuilder) From(table string) *QueryBuilder {
    q.table = table
    return q
}

func (q *QueryBuilder) Where(condition string) *QueryBuilder {
    q.wheres = append(q.wheres, condition)
    return q
}

func (q *QueryBuilder) Limit(n int) *QueryBuilder {
    q.limit = n
    return q
}

func (q *QueryBuilder) Build() string {
    sql := fmt.Sprintf("SELECT * FROM %s", q.table)
    if len(q.wheres) > 0 {
        sql += " WHERE " + strings.Join(q.wheres, " AND ")
    }
    if q.limit > 0 {
        sql += fmt.Sprintf(" LIMIT %d", q.limit)
    }
    return sql
}

// Usage
query := (&QueryBuilder{}).
    From("users").
    Where("age > 18").
    Where("active = true").
    Limit(100).
    Build()
// SELECT * FROM users WHERE age > 18 AND active = true LIMIT 100

The chaining pattern works because each method returns *QueryBuilder, allowing the next call immediately on the result.

Methods on Non-Struct Types

Methods work on any named type, not just structs. This is useful for adding behavior to primitive-based types:

type Duration int64  // represents nanoseconds

func (d Duration) Seconds() float64 { return float64(d) / 1e9 }
func (d Duration) String() string   { return fmt.Sprintf("%.2fs", d.Seconds()) }

type Celsius float64

func (c Celsius) ToFahrenheit() float64 { return float64(c)*9/5 + 32 }
func (c Celsius) String() string        { return fmt.Sprintf("%.1f°C", c) }

And on slice types for sorting and filtering:

type UserList []User

func (ul UserList) Len() int           { return len(ul) }
func (ul UserList) Less(i, j int) bool { return ul[i].Name < ul[j].Name }
func (ul UserList) Swap(i, j int)      { ul[i], ul[j] = ul[j], ul[i] }

// UserList now satisfies sort.Interface
sort.Sort(UserList(users))

Summary

  • Value receiver: gets a copy; use for reads, small immutable types, String() and Error() by convention
  • Pointer receiver: gets a pointer; required for mutations, large structs, any type with internal state
  • The consistency rule: if any method uses *T, use *T for all methods on that type
  • Only *T satisfies an interface when the implementing method has a pointer receiver — T does not
  • Go auto-takes address for addressable values, but not for map elements or function return values
  • Methods work on any named type, not just structs — enables adding behavior to slices, primitives, and aliases

Resources

Comments

👍 Was this article helpful?