go
This post is part of a learning series: Go Deep

Understanding T and *T method receivers in Go

Posted by Vikash Patel on Sunday, Feb 23, 2025 Reading time: 3 Min

In Go, method receivers determine whether a method acts on a copy of a value or a reference to it. This choice isn’t just about performance—it affects correctness and behavior, especially when dealing with synchronization primitives (mutex, wait group, etc), slices, and embedded types.

In this post I explore when to use T (a value receiver) vs. *T (a pointer receiver) and why, in most cases, pointer receivers should be the default and preferred.


Understanding T and *T

In Go, every type T has an associated pointer type *T. Taking the address of a T variable results in a *T:

type T struct {
    a int
    b bool
}

var t T     // t is of type T
var p = &t  // p is of type *T

T and *T are distinct types. You can’t substitute *T where T is expected and vice versa.

Declaring Methods on Types

Go allows you to declare methods on any type you define in your package. That means you can attach methods to both T and *T:

func (t T) ValueMethod() { /* operates on a copy */ }
func (t *T) PointerMethod() { /* operates on the original */ }

Now the billion dollar question: When should a method use a pointer receiver?

When to Use a Pointer Receiver

1. The method modifies the receiver.

If your method changes the state of T, you must use *T, or modifications won’t persist.

func (p *Person) Rename(name string) {
    p.Name = name
}
2. The type is large.

Passing large structs by value incurs unnecessary copying.

type BigStruct struct {
    Data [1024]byte
}

func (b BigStruct) BadMethod() { /* Copies 1024 bytes */ }

func (b *BigStruct) GoodMethod() { /* Uses reference of the 1024 bytes */ }
3. The type contains synchronization primitives (e.g., sync.Mutex).

Copying a sync.Mutex defeats it purpose of maintaining the state.

type Counter struct {
    mu  sync.Mutex
    val int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.val++
}

Declaring Increment on Counter (not *Counter) would copy mu, making it useless.

4. The type is meant to be used via pointers.

Some types naturally work as pointers, such as linked list nodes.

5. The type is embedded in another struct.

If a type is meant to be embedded, methods must use pointer receivers to avoid unintended copies. Copies introduce performance penalty.

type Stats struct {
    a, b, c Counter
}

func (s Stats) Sum() int {
    return s.a.Increment() + s.b.Increment() + s.c.Increment() // Copies `Counter`, breaks mutex
}

When To Use a Value Receiver?

A value receiver is fine if:

  • The method does not modify the receiver.
  • The type is small and simple (like int, bool, or struct{X, Y int}).
  • The type is inherently immutable.

For example:

type Point struct {
    X, Y int
}

func (p Point) Distance(q Point) float64 {
    dx, dy := float64(q.X-p.X), float64(q.Y-p.Y)
    return math.Sqrt(dx*dx + dy*dy)
}

Here, Distance doesn’t modify Point, and copying two ints is negligible.

Should Methods That Don’t Mutate Use a Pointer Receiver?

Yes, in most cases. Even if a method doesn’t modify the receiver, using *T avoids unintended copies and maintains consistency across your codebase. The exceptions:

  • Small, immutable types (e.g., Point).
  • Types that should be copied intentionally, like function options or temporary configurations.

Basically:

  • Default to using *T (reference receiver) unless you have a good reason not to.
  • Use T (value receiver) only when your type is small and meant to be copied.
  • Never use T (value receiver) if the type has a sync.Mutex, a slice, or is embedded in another struct.

By following these guidelines, you avoid subtle bugs and unintended performance penalties in your Go code.

Cheers!! Happy coding.