Go Methods Tutorial
Methods in Go are functions that operate on specific types (receivers). They provide a way to associate behavior with data structures, enabling object-oriented programming patterns while maintaining Go's simplicity and type safety.
1. Method Fundamentals
1.1 Method Declaration
Methods are declared with a special receiver parameter that appears between the func keyword and the method name.
// Basic method syntax
func (receiver ReceiverType) MethodName(parameters) returnTypes {
// Method body
}
// Example with Rectangle type
type Rectangle struct {
Width, Height float64
}
// Area method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Perimeter method with value receiver
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
1.2 Method Invocation
Methods are called using dot notation on an instance of the receiver type.
rect := Rectangle{Width: 10, Height: 5}
// Calling methods
area := rect.Area()
perimeter := rect.Perimeter()
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", area, perimeter)
// Output: Area: 50.00, Perimeter: 30.00
1.3 Value vs Pointer Receivers
Methods can be declared with either value or pointer receivers, which affects whether they can modify the receiver.
// Value receiver (works on a copy)
func (r Rectangle) DoubleWidth() {
r.Width *= 2 // Won't affect original
}
// Pointer receiver (works on original)
func (r *Rectangle) DoubleHeight() {
r.Height *= 2 // Modifies original
}
// Usage
rect := Rectangle{10, 5}
rect.DoubleWidth()
fmt.Println(rect.Width) // Still 10
rect.DoubleHeight()
fmt.Println(rect.Height) // Now 10
2. Choosing Receiver Types
2.1 When to Use Value Receivers
Value receivers are appropriate when:
- The method doesn't need to modify the receiver
- The receiver type is small (copying is cheap)
- You want to guarantee immutability
type Point struct {
X, Y float64
}
// Value receiver - calculates distance without modifying Point
func (p Point) DistanceToOrigin() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
2.2 When to Use Pointer Receivers
Pointer receivers are necessary when:
- The method needs to modify the receiver
- The receiver type is large (avoid copying overhead)
- The type contains synchronization primitives (e.g., mutexes)
type BankAccount struct {
balance float64
mutex sync.Mutex
}
// Pointer receiver - modifies account and uses mutex
func (a *BankAccount) Deposit(amount float64) {
a.mutex.Lock()
defer a.mutex.Unlock()
a.balance += amount
}
2.3 Consistency in Receiver Types
For a given type, it's good practice to be consistent with receiver types:
type Counter struct {
value int
}
// All methods use pointer receivers for consistency
func (c *Counter) Increment() {
c.value++
}
func (c *Counter) Decrement() {
c.value--
}
func (c *Counter) Value() int {
return c.value
}
3. Methods on Non-Struct Types
3.1 Methods on Basic Types
You can define methods on any type declared in your package, including basic types.
// Declare a new type based on int
type Celsius float64
// Method to convert Celsius to Fahrenheit
func (c Celsius) Fahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
// Usage
temp := Celsius(100)
fmt.Println(temp.Fahrenheit()) // 212
3.2 Methods on Slice Types
Custom slice types can have methods for specialized operations.
type StringSlice []string
// Method to check if slice contains a string
func (ss StringSlice) Contains(s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
}
// Usage
names := StringSlice{"Alice", "Bob", "Charlie"}
fmt.Println(names.Contains("Bob")) // true
3.3 Methods on Function Types
You can even attach methods to function types for advanced patterns.
type Predicate func(int) bool
// Method to negate a predicate
func (p Predicate) Negate() Predicate {
return func(n int) bool {
return !p(n)
}
}
// Usage
isEven := Predicate(func(n int) bool { return n%2 == 0 })
isOdd := isEven.Negate()
fmt.Println(isEven(4), isOdd(4)) // true false
4. Method Sets and Interfaces
4.1 Understanding Method Sets
A type's method set determines which methods can be called on values of that type.
type Shape interface {
Area() float64
Perimeter() float64
}
// Rectangle satisfies Shape implicitly
func (r Rectangle) Area() float64 { /* ... */ }
func (r Rectangle) Perimeter() float64 { /* ... */ }
// Circle also satisfies Shape
type Circle struct { Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
4.2 Pointer vs Value Method Sets
The method set for *T includes all methods declared with both value and pointer receivers.
type Counter struct { value int }
func (c Counter) Value() int { return c.value }
func (c *Counter) Increment() { c.value++ }
// Value method set: Value()
// Pointer method set: Value(), Increment()
var c Counter
c.Increment() // Works - Go automatically takes address
(&c).Value() // Also works
4.3 Interface Satisfaction Rules
Key rules for interface implementation:
Timplements all methods declared withTreceiver*Timplements all methods declared with eitherTor*Treceiver- Interface values can hold either
Tor*TifTimplements the interface
5. Advanced Method Patterns
5.1 Method Chaining
Methods can return the receiver to enable fluent interfaces.
type StringBuilder struct {
buffer strings.Builder
}
func (sb *StringBuilder) WriteString(s string) *StringBuilder {
sb.buffer.WriteString(s)
return sb
}
func (sb *StringBuilder) String() string {
return sb.buffer.String()
}
// Usage
result := new(StringBuilder).
WriteString("Hello").
WriteString(" ").
WriteString("World").
String()
5.2 Factory Methods
Methods can act as factories for related types.
type Matrix struct {
data [][]float64
}
// Factory method on Matrix to create identity matrix
func (m Matrix) Identity(size int) Matrix {
identity := make([][]float64, size)
for i := range identity {
identity[i] = make([]float64, size)
identity[i][i] = 1.0
}
return Matrix{data: identity}
}
5.3 Method Expressions
Methods can be referenced as first-class functions.
// Method expression syntax: Type.MethodName
areaFunc := Rectangle.Area // Type: func(Rectangle) float64
rect := Rectangle{10, 5}
fmt.Println(areaFunc(rect)) // 50
// Can also work with pointer receivers
scaleFunc := (*Rectangle).DoubleHeight
scaleFunc(&rect)
fmt.Println(rect.Height) // 10
6. Performance Considerations
6.1 Receiver Size Impact
The size of the receiver affects method call performance.
// Small struct - value receiver is fine
type Point struct { X, Y float64 } // 16 bytes
// Large struct - pointer receiver preferred
type Image struct {
pixels [1000][1000]byte // 1,000,000 bytes
}
func (p Point) Distance() float64 { /* ... */ } // OK
func (img *Image) Process() { /* ... */ } // Better
6.2 Inlining Opportunities
Small methods may be inlined by the compiler for better performance.
// Likely to be inlined
func (p Point) Add(q Point) Point {
return Point{p.X + q.X, p.Y + q.Y}
}
// Less likely to be inlined
func (img *Image) ComplexOperation() {
// Many lines of complex code
}
6.3 Interface Method Calls
Interface method calls have a small runtime overhead compared to direct calls.
// Direct call (fastest)
rect := Rectangle{10, 5}
area := rect.Area()
// Interface call (slightly slower)
var shape Shape = rect
area = shape.Area()
7. Common Method Idioms
7.1 String Representation
The String() method customizes how a type is printed.
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}
// Usage
person := Person{"Alice", 30}
fmt.Println(person) // Alice (30 years)
7.2 Error Reporting
The Error() method enables custom error types.
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// Usage
err := ValidationError{"Email", "invalid format"}
fmt.Println(err) // Email: invalid format
7.3 Sorting Interface
Methods can implement standard interfaces like sort.Interface.
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
// Usage
people := []Person{{"Bob", 31}, {"Alice", 25}}
sort.Sort(ByAge(people))