Overview
Methods in Go allow you to attach behavior to types. The crucial decision you’ll make is choosing between Value Receivers and Pointer Receivers . This choice determines whether your methods can modify state and affects performance.
The Fundamental Question
Every method forces you to answer:
“Does this method need to change the thing it’s called on?”
Yes → Use a pointer receiver (*Type)
No → Use a value receiver (Type)
Value vs Pointer Receivers
Value Receiver
Pointer Receiver
A value receiver gets a copy of the struct. package main
import " fmt "
type side struct {
l , b int
}
func ( s side ) area () int {
return s . l * s . b
}
func ( s side ) para () int {
return 2 * ( s . l + s . b )
}
func main () {
s := side { 10 , 20 }
fmt . Println ( s . area (), s . para ()) // 200 60
}
When to use:
Reading data (getters, calculations)
Small structs (avoid copy overhead)
When you want immutability
Changes made inside the method don’t affect the original struct.
A pointer receiver gets a reference to the actual struct. package main
import " fmt "
type Value struct {
x , y int
}
func ( v * Value ) change () {
v . x = v . x * 10
v . y = v . y * 10
}
func ( v Value ) area () int {
return v . x * v . y
}
func main () {
a := Value { 10 , 20 }
a . change () // Modifies a
fmt . Println ( a . area ()) // 20000 (100 * 200)
}
When to use:
Modifying the receiver (setters, state changes)
Large structs (avoid copy overhead)
Consistency (if any method uses pointer, use it everywhere)
All methods on a type should use the same receiver type for consistency. Mixing can be confusing.
Real-World Example: Bank Account
Let’s implement a bank account that demonstrates why pointer receivers are essential for state mutation.
package main
import " fmt "
type Account struct {
balance int
}
func ( a * Account ) deposit ( money int ) {
a . balance += money
}
func ( a * Account ) withdrawl ( money int ) {
a . balance -= money
if a . balance < 0 {
a . balance = 0
}
}
func ( a Account ) getBalance () int {
return a . balance
}
func main () {
v := Account { 100 }
fmt . Println ( v . getBalance ()) // 100
v . deposit ( 100 )
fmt . Println ( v . getBalance ()) // 200
v . withdrawl ( 100 )
fmt . Println ( v . getBalance ()) // 100
}
deposit() and withdrawl() use pointer receivers
These methods MUST modify the balance, so they need *Account. If we used a value receiver, the money would disappear after the function ends!
getBalance() uses a value receiver
This method only reads the balance, so a value receiver is safe. This signals to other developers: “This method doesn’t change anything.”
Getters and Setters
Go doesn’t have public/private keywords. Instead, we use exported (capitalized) and unexported (lowercase) names.
package main
import " fmt "
type FeatureFlag struct {
enabled bool
}
func ( f * FeatureFlag ) enable () {
f . enabled = true
}
func ( f * FeatureFlag ) disable () {
f . enabled = false
}
func ( f FeatureFlag ) isEnabled () bool {
return f . enabled
}
func main () {
v := FeatureFlag { false }
v . enable ()
fmt . Println ( v . isEnabled ()) // true
v . disable ()
fmt . Println ( v . isEnabled ()) // false
}
Design Principles:
Setters (enable, disable) → Pointer receivers (write state)
Getters (isEnabled) → Value receiver (read state)
In Go, getters don’t use the Get prefix. Instead of GetBalance(), use Balance().
Command-Query Separation
A clean API design principle: methods should either do something OR return something , but rarely both.
package main
import " fmt "
type Vertex struct {
a , b int
}
// COMMAND: Changes state, returns nothing
func ( v * Vertex ) scale ( f int ) {
v . a = v . a * f
v . b = v . b * f
}
// QUERY: Reads state, returns a value
func ( v * Vertex ) abs () int {
return v . a * v . b
}
func main () {
v := Vertex { 10 , 20 }
v . scale ( 2 ) // command: mutate state
fmt . Println ( v . abs ()) // query: read state (400)
}
Why Separate Commands and Queries?
Clarity - You immediately know if a method has side effects
Testing - Queries can be called multiple times without changing state
Concurrency - Queries are naturally thread-safe (if the data is immutable)
Composition - Commands can be sequenced, queries can be chained
Methods vs Functions
Go provides syntactic sugar for methods that doesn’t exist for functions.
package main
import " fmt "
type Vertex struct {
X , Y float64
}
// METHOD
func ( v * Vertex ) Scale ( f float64 ) {
v . X = v . X * f
v . Y = v . Y * f
}
// FUNCTION
func ScaleFunc ( v * Vertex , f float64 ) {
v . X = v . X * f
v . Y = v . Y * f
}
func main () {
v := Vertex { 3 , 4 }
v . Scale ( 2 ) // ✅ Works with value
ScaleFunc ( & v , 10 ) // ✅ Must pass pointer explicitly
p := & Vertex { 4 , 3 }
p . Scale ( 3 ) // ✅ Works with pointer (Go auto-dereferences)
ScaleFunc ( p , 8 ) // ✅ Pointer matches function signature
fmt . Println ( v , p )
}
The Magic of Methods
Methods (Flexible)
Functions (Strict)
// Go automatically handles conversions:
v := Vertex { 3 , 4 }
v . Scale ( 2 ) // v.Scale(&v, 2) behind the scenes
p := & Vertex { 3 , 4 }
p . Scale ( 2 ) // (*p).Scale(2) behind the scenes
Methods are syntactic sugar. v.Scale() works whether v is a value or pointer. Functions require exact type matches.
When to Use Each Receiver Type
Use Pointer Receivers
Use Value Receivers
Choose *Type when:
The method modifies the receiver
func ( a * Account ) Deposit ( amount int ) {
a . balance += amount // Modifying state
}
The struct is large
type LargeStruct struct {
data [ 1000000 ] byte
}
func ( l * LargeStruct ) Process () {
// Avoid copying 1MB on every call
}
Consistency with other methods
type User struct {
name string
}
func ( u * User ) SetName ( name string ) { /* ... */ }
// Keep consistent even for read-only methods
func ( u * User ) Name () string { return u . name }
Choose Type when:
The method only reads data
func ( p Point ) Distance () float64 {
return math . Sqrt ( p . X * p . X + p . Y * p . Y )
}
The struct is small (2-3 fields)
type Point struct {
X , Y int
}
func ( p Point ) Add ( q Point ) Point {
return Point { p . X + q . X , p . Y + q . Y }
}
You want immutability
type Config struct {
timeout time . Duration
}
func ( c Config ) WithTimeout ( d time . Duration ) Config {
c . timeout = d
return c // Returns a new copy
}
Common Patterns
Encapsulation Pattern
Bundle data and behavior together:
type Counter struct {
value int // unexported field (private)
}
func NewCounter () * Counter {
return & Counter { value : 0 }
}
func ( c * Counter ) Inc () {
c . value ++
}
func ( c * Counter ) Add ( n int ) {
c . value += n
}
func ( c Counter ) Value () int {
return c . value
}
Builder Pattern
Chain method calls by returning the receiver:
type QueryBuilder struct {
table string
where string
limit int
}
func ( q * QueryBuilder ) Table ( name string ) * QueryBuilder {
q . table = name
return q
}
func ( q * QueryBuilder ) Where ( condition string ) * QueryBuilder {
q . where = condition
return q
}
func ( q * QueryBuilder ) Limit ( n int ) * QueryBuilder {
q . limit = n
return q
}
// Usage:
query := new ( QueryBuilder ).
Table ( "users" ).
Where ( "age > 18" ).
Limit ( 10 )
Best Practices
Be consistent
If any method needs a pointer receiver, use pointer receivers for all methods on that type.
Follow naming conventions
Don’t use Get prefix: Balance() not GetBalance()
Use short receiver names: (u *User) not (user *User)
Keep receiver names consistent: always u for User, not sometimes usr
Think about copies
Value receivers copy the entire struct. For large structs, this can be expensive.
Consider interfaces
If your type implements an interface, be aware that methods with pointer receivers won’t work with value types. type Printer interface {
Print ()
}
type Doc struct {}
func ( d * Doc ) Print () {} // Only *Doc implements Printer
var p Printer = Doc {} // ❌ Compile error
var p Printer = & Doc {} // ✅ Correct
Key Takeaways
Pointer receivers (*Type) allow mutation and avoid copies
Value receivers (Type) provide safety and immutability
Methods get automatic pointer/value conversion, functions don’t
Keep all methods on a type consistent (all pointer or all value)
Command-Query Separation makes code more predictable
Next Steps