Skip to content
C Codeloom
Go

Go Structs and Methods: Value vs Pointer Receivers

A clear guide to Go structs and methods: literal forms, defining methods, choosing value vs pointer receivers, and using embedding for composition.

·6 min read · By Yash Kesharwani
Intermediate 10 min read

What you'll learn

  • How to declare and instantiate structs in Go
  • How to attach methods to types
  • When to use value vs pointer receivers
  • How embedding provides composition without inheritance
  • Common idioms for constructors and zero values

Prerequisites

  • Comfortable with [Go variables and types](/blog/go-variables-and-types)
  • Familiar with [Go functions](/blog/go-functions)

Structs are the workhorse data type of Go. They hold named fields, support methods, and compose through embedding. Once you understand how struct values are passed, how methods bind to types, and when to choose pointer receivers, almost every other piece of Go code becomes easier to read.

Declaring a struct

A struct type lists its fields and their types:

type User struct {
	ID    int
	Name  string
	Email string
}

Field names follow the same exported/unexported rule as everything else: uppercase to expose, lowercase to keep private.

Literal forms

There are three common ways to create a struct value:

// Named fields (recommended)
u1 := User{ID: 1, Name: "Ada", Email: "a@b.c"}

// Positional (fragile; breaks if fields are reordered)
u2 := User{2, "Linus", "l@kernel.org"}

// Zero value, then assignment
var u3 User
u3.ID = 3
u3.Name = "Grace"

Always prefer the named form. The zero value of a struct is each field’s zero value, which is often a useful starting state.

Pointers to structs

Passing a struct by value copies every field. For small structs that is fine; for larger ones, or when you need mutation to be visible to the caller, use a pointer.

func grow(u *User) {
	u.Name = "Grown " + u.Name
}

u := User{Name: "seed"}
grow(&u)
fmt.Println(u.Name) // Grown seed

Inside grow, u.Name is automatic shorthand for (*u).Name; Go dereferences pointers in field access for you.

Constructors

Go has no constructors as a language feature, just functions that return values. The convention is New<Type>:

func NewUser(name, email string) *User {
	return &User{Name: name, Email: email}
}

Return a pointer when the caller will typically mutate the result, or when zero-allocation hands are not a concern. Otherwise returning the value is fine.

Methods

A method is a function with a receiver, written between func and the method name:

type Rectangle struct{ W, H float64 }

func (r Rectangle) Area() float64 {
	return r.W * r.H
}

func main() {
	r := Rectangle{W: 3, H: 4}
	fmt.Println(r.Area())
}

The receiver acts like a regular parameter. The expression r.Area() is sugar for calling Area with r as the receiver.

Value vs pointer receivers

This is the single most important choice when adding methods to a struct.

A value receiver gets a copy of the struct. Methods cannot mutate the original.

func (r Rectangle) Scale(f float64) {
	r.W *= f // modifies the copy only
}

A pointer receiver gets a pointer; methods can mutate the original and avoid copying large structs.

func (r *Rectangle) Scale(f float64) {
	r.W *= f
	r.H *= f
}

Guidelines that work in practice:

  • If any method on the type needs a pointer receiver, use pointer receivers for all methods on that type. Consistency lets the type satisfy interfaces predictably.
  • Use a pointer receiver if the method mutates state.
  • Use a pointer receiver if the struct is large enough that copying is wasteful (say, more than a few words).
  • Use a pointer receiver if the struct contains a sync.Mutex or other field that must not be copied.
  • Use a value receiver only for small, immutable, value-like types (think coordinates, durations, complex numbers).

Method sets and interfaces

The method set of T includes methods with value receivers. The method set of *T includes methods with both value and pointer receivers. This matters for interface satisfaction:

type Stringer interface{ String() string }

type Color struct{ R, G, B uint8 }

func (c *Color) String() string {
	return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}

var s Stringer = &Color{255, 128, 0} // ok
// var s Stringer = Color{255, 128, 0} // compile error

Color only has the method via *Color, so only *Color satisfies Stringer.

Embedding for composition

Go has no inheritance. Instead, you embed one type inside another, and the outer type gets the inner type’s fields and methods automatically.

type Logger struct{}

func (Logger) Log(msg string) { fmt.Println("LOG:", msg) }

type Service struct {
	Logger // embedded
	Name   string
}

func main() {
	s := Service{Name: "billing"}
	s.Log("hello") // promoted from Logger
}

s.Log is promoted from the embedded Logger. You can still reach the inner value explicitly as s.Logger if a name conflict arises.

Embedding is the workhorse of code reuse in Go. It is also how you wrap and extend: embed an interface or struct, override the methods you want, and delegate the rest implicitly.

type CountingLogger struct {
	Logger
	count int
}

func (c *CountingLogger) Log(msg string) {
	c.count++
	c.Logger.Log(msg)
}

A complete example

package main

import "fmt"

type Money struct {
	Amount   int64 // in cents
	Currency string
}

func (m Money) String() string {
	return fmt.Sprintf("%d.%02d %s", m.Amount/100, m.Amount%100, m.Currency)
}

type Invoice struct {
	ID    string
	Items []Money
}

func (i *Invoice) Total() Money {
	if len(i.Items) == 0 {
		return Money{Currency: "USD"}
	}
	var sum int64
	for _, m := range i.Items {
		sum += m.Amount
	}
	return Money{Amount: sum, Currency: i.Items[0].Currency}
}

func main() {
	inv := &Invoice{
		ID: "A-1",
		Items: []Money{
			{Amount: 1999, Currency: "USD"},
			{Amount: 500, Currency: "USD"},
		},
	}
	fmt.Println(inv.Total())
}

Money is a small value type with a value receiver; Invoice carries a slice and is used through a pointer. The combination scales well to bigger systems and is comfortable to test.

For patterns that loop over collections of structs, see Go slices and maps.

Wrap up

Structs and methods are how Go represents domain types. Pick named field literals, lean on zero values, and decide receiver kind based on mutation, size, and consistency. When you need to share behavior, embed instead of inheriting; when you need to expose behavior, design small methods that compose into interfaces. Get these instincts right and the rest of your Go code will follow naturally.