Skip to content

Go Methods — Value Receivers Pointer Receivers and Method Sets Explained

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Go Methods. We cover key concepts, practical examples, and best practices to help you master this topic.

Go methods are functions with a receiver argument that can be a value or pointer type, enabling object-oriented behavior without classes through method sets, interface satisfaction, and receiver choice.

What You'll Learn

  • Defining methods on types
  • Value vs pointer receivers
  • Method sets and rules
  • When to use each receiver type

Why It Matters

Methods are Go's approach to object-oriented programming. Docker uses methods extensively on container and image types. Kubernetes defines methods on resource types. DodaZIP uses methods for archive manipulation. Understanding receivers is fundamental to Go.

Real-World Use

Types like http.ResponseWriter, sql.DB, and custom types all use methods. Methods enable Encapsulation, interface satisfaction, and clean APIs.

flowchart LR
    A["Methods"] --> B["Value Receiver"]
    B --> C["Pointer Receiver"]
    C --> D["Method Sets"]
    D --> E["Interfaces"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Defining Methods

Methods are functions with a receiver parameter:

type Rectangle struct {
    Width, Height float64
}

// Value receiver method
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Pointer receiver method
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println(rect.Area())  // 50

    rect.Scale(2)
    fmt.Println(rect.Area())  // 200
}

Value vs Pointer Receivers

Value Receiver

type Counter struct {
    value int
}

func (c Counter) Value() int {
    return c.value
}

func (c Counter) Increment() {
    c.value++  // Modifies copy, not original!
}

func main() {
    c := Counter{value: 10}
    fmt.Println(c.Value())    // 10

    c.Increment()
    fmt.Println(c.Value())    // 10 — unchanged!
}

Pointer Receiver

type Counter struct {
    value int
}

func (c *Counter) Increment() {
    c.value++  // Modifies original
}

func (c *Counter) Reset() {
    c.value = 0
}

func main() {
    c := &Counter{value: 10}
    c.Increment()
    fmt.Println(c.value)  // 11

    c.Reset()
    fmt.Println(c.value)  // 0
}

Method Sets

The method set determines which methods satisfy an interface:

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

// Value receiver
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func main() {
    var s Shape

    // Circle value satisfies Shape
    c := Circle{Radius: 5}
    s = c
    fmt.Println(s.Area())  // 78.5398...

    // Circle pointer also satisfies Shape
    cp := &Circle{Radius: 3}
    s = cp
    fmt.Println(s.Area())  // 28.2743...
}

Method Set Rules

type Point struct {
    X, Y float64
}

// Value receiver
func (p Point) Distance() float64 {
    return math.Sqrt(p.X*p.X + p.Y*p.Y)
}

// Pointer receiver
func (p *Point) Move(x, y float64) {
    p.X += x
    p.Y += y
}

func main() {
    p := Point{X: 3, Y: 4}

    // Value receiver works with both value and pointer
    fmt.Println(p.Distance())   // 5
    fmt.Println((&p).Distance()) // 5 — Go auto-dereferences

    // Pointer receiver works with pointer
    (&p).Move(1, 2)

    // Go auto-references for pointer methods
    p.Move(1, 2)  // Same as (&p).Move(1, 2)
    fmt.Println(p)  // {5, 8}
}

Methods on Any Type

Methods work on any type, not just structs:

type Celsius float64

func (c Celsius) ToFahrenheit() float64 {
    return float64(c)*9.0/5.0 + 32
}

func (c Celsius) IsFreezing() bool {
    return c <= 0
}

type StringList []string

func (sl StringList) Contains(target string) bool {
    for _, s := range sl {
        if s == target {
            return true
        }
    }
    return false
}

func (sl *StringList) Add(items ...string) {
    *sl = append(*sl, items...)
}

func main() {
    temp := Celsius(25)
    fmt.Println(temp.ToFahrenheit())  // 77
    fmt.Println(Celsius(-5).IsFreezing())  // true

    list := StringList{"a", "b", "c"}
    fmt.Println(list.Contains("b"))  // true
    list.Add("d", "e")
    fmt.Println(list.Contains("e"))  // true
}

Methods with Named Return Values

type Temperature struct {
    Celsius float64
}

func (t Temperature) Fahrenheit() (f float64) {
    f = t.Celsius*9.0/5.0 + 32
    return  // Named return — returns f
}

func (t *Temperature) SetFromFahrenheit(f float64) {
    t.Celsius = (f - 32) * 5.0 / 9.0
}

Methods and Pointers

type Config struct {
    Host string
    Port int
}

func (c Config) Address() string {
    return fmt.Sprintf("%s:%d", c.Host, c.Port)
}

func (c *Config) SetDefaults() {
    if c.Host == "" {
        c.Host = "localhost"
    }
    if c.Port == 0 {
        c.Port = 8080
    }
}

func main() {
    // Creating pointer to struct
    c := &Config{}
    c.SetDefaults()
    fmt.Println(c.Address())  // localhost:8080

    // Creating value and taking address
    c2 := Config{Host: "example.com", Port: 443}
    p := &c2
    fmt.Println(p.Address())  // example.com:443
}

Common Mistakes

1. Modifying Value Receiver Expecting Original to Change

type User struct {
    Name string
}

func (u User) SetName(name string) {
    u.Name = name  // Modifies copy!
}

func main() {
    u := User{Name: "Alice"}
    u.SetName("Bob")
    fmt.Println(u.Name)  // Alice — unchanged!
}

2. Inconsistent Receiver Type

type Stats struct{}

// Mixing receiver types is allowed but can be confusing
func (s Stats) Read() string  { return "data" }
func (s *Stats) Write(d string) {}  // Mixing pointer and value

3. Not Using Pointer for Large Structs

type LargeData struct {
    // Many fields — 1KB+
}

// Copies the entire struct on every call
func (d LargeData) Process() {}  // Expensive copy

// No copy — better
func (d *LargeData) Process() {}  // Just a pointer

4. Methods on nil Pointers

type Tree struct {
    Value int
    Left  *Tree
    Right *Tree
}

func (t *Tree) Sum() int {
    if t == nil {
        return 0  // Handle nil receiver!
    }
    return t.Value + t.Left.Sum() + t.Right.Sum()
}

5. Forgetting Method Cannot Modify Map or Slice Header

type Items struct {
    data []string
}

func (i Items) Add(item string) {
    i.data = append(i.data, item)  // Modifies copy of slice header!
}

// Fix: use pointer receiver
func (i *Items) Add(item string) {
    i.data = append(i.data, item)
}

Practice Questions

1. What is the difference between a value receiver and a pointer receiver?

Value receiver operates on a copy of the original — modifications don't persist. Pointer receiver operates on the original — modifications persist. Pointer receiver is also more efficient for large types.

2. Can you call a pointer receiver method on a value?

Yes, Go automatically takes the address for pointer receiver methods when the value is addressable. p.Move() works even if Move has a pointer receiver, as Go converts it to (&p).Move().

3. What is a method set?

The set of methods defined on a type. Value type T has methods with value receivers. Pointer type *T has methods with both value and pointer receivers.

4. Can you define methods on built-in types?

Not directly. You must create a named type based on the built-in: type MyInt int. Then define methods on MyInt.

Challenge: Implement a Stack type with methods Push, Pop, Peek, and IsEmpty, supporting any element type using generics.

Solution
package main

import "fmt"

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if s.IsEmpty() {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func (s *Stack[T]) Peek() (T, bool) {
    if s.IsEmpty() {
        var zero T
        return zero, false
    }
    return s.items[len(s.items)-1], true
}

func (s *Stack[T]) IsEmpty() bool {
    return len(s.items) == 0
}

func main() {
    stack := Stack[int]{}
    stack.Push(1)
    stack.Push(2)
    stack.Push(3)

    for !stack.IsEmpty() {
        item, _ := stack.Pop()
        fmt.Println(item)
    }
    // 3
    // 2
    // 1
}

FAQ

{{< faq question="When should I use a pointer receiver?" >}} Use pointer receiver when: modifying the receiver, the struct is large (avoid copying), or you need consistency (some methods are pointer receivers). Use value receiver for small, immutable types. {{< /faq >}}

{{< faq question="Can a method have both value and pointer receivers?" >}} No. Each method must have exactly one receiver type. But a type can have some methods with value receivers and others with pointer receivers. {{< /faq >}}

{{< faq question="What happens when you call a pointer method on a non-addressable value?" >}} Compile error. Non-addressable values (return values, map values) cannot have their address taken. Always use pointer receivers consistently. {{< /faq >}}

{{< faq question="Can methods be overloaded?" >}} No. Go doesn't support method overloading. Each method name must be unique within a type's method set. Use different names or variadic parameters. {{< /faq >}}

{{< faq question="How do methods relate to interfaces?" >}} A type satisfies an interface if it implements all of the interface's methods. The method set determines interface satisfaction. Pointer types have a larger method set (include value receiver methods). {{< /faq >}}

Try It Yourself

package main

import (
    "fmt"
    "math"
)

type Vec2 struct {
    X, Y float64
}

// Value receiver — read-only
func (v Vec2) Length() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// Pointer receiver — mutation
func (v *Vec2) Normalize() {
    l := v.Length()
    if l > 0 {
        v.X /= l
        v.Y /= l
    }
}

func (v *Vec2) Add(other Vec2) {
    v.X += other.X
    v.Y += other.Y
}

func main() {
    v := Vec2{X: 3, Y: 4}
    fmt.Printf("Length: %.2f\n", v.Length())  // 5.00

    v.Normalize()
    fmt.Printf("Normalized: (%.2f, %.2f)\n", v.X, v.Y)  // (0.60, 0.80)

    v.Add(Vec2{X: 1, Y: 1})
    fmt.Printf("After add: (%.2f, %.2f)\n", v.X, v.Y)  // (1.60, 1.80)
}

Expected output:

Length: 5.00
Normalized: (0.60, 0.80)
After add: (1.60, 1.80)

What's Next

Now that you understand methods, learn about interfaces — Go's approach to polymorphism and abstraction.

Topic Description Link
Go Interfaces Interface types, satisfaction, type assertions {{< ref "11-interfaces" >}}
Go Embedding Struct embedding, method promotion {{< ref "12-embedding" >}}
Rust Traits Compare with Rust's trait system Rust

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go