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.
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.Mutexor 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.