Skip to main content

Go Ep 8: Methods & Struct Composition

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
go - This article is part of a series.
Part 8: This Article
Go favors Composition over Inheritance. By embedding structs inside other structs, you automatically promote fields and methods without the rigid coupling of object-oriented class hierarchies.

TL;DR (Quick Summary)
#

  • Methods: Functions attached to a specific type via a Receiver clause (func (r ReceiverType) MethodName()).
  • Value Receiver (r Type): Operates on a copy of the struct. Cannot mutate original fields.
  • Pointer Receiver (r *Type): Operates directly on the memory address. Can mutate struct fields and avoids copying overhead.
  • Struct Embedding: Anonymous struct fields promote their inner methods and fields to the outer container struct automatically.

1. Value Receiver vs Pointer Receiver
#

package main

import "fmt"

type Account struct {
	Owner   string
	Balance float64
}

// Value Receiver (Operates on a COPY - Balance NOT updated!)
func (a Account) DepositCopy(amount float64) {
	a.Balance += amount
}

// Pointer Receiver (Operates on ORIGINAL memory - Balance IS updated!)
func (a *Account) Deposit(amount float64) {
	a.Balance += amount
}

func main() {
	acc := Account{Owner: "Rachmat", Balance: 100.0}

	acc.DepositCopy(50.0)
	fmt.Println("After Copy Deposit:", acc.Balance) // 100.0

	acc.Deposit(50.0)
	fmt.Println("After Pointer Deposit:", acc.Balance) // 150.0
}

2. Struct Embedding (Composition over Inheritance)
#

Instead of subclassing, Go uses Struct Embedding to promote inner struct capabilities.

package main

import "fmt"

// Base struct
type Logger struct {
	Prefix string
}

func (l *Logger) Log(message string) {
	fmt.Printf("[%s] %s
", l.Prefix, message)
}

// Outer struct EMBEDS Logger anonymously
type UserService struct {
	Logger // Embedded struct!
	DBName string
}

func main() {
	service := UserService{
		Logger: Logger{Prefix: "USER-SVC"},
		DBName: "production_db",
	}

	// 🟢 Log() method is automatically PROMOTED to service!
	service.Log("Initializing database connection...")
	// Output: [USER-SVC] Initializing database connection...
}

3. Receiver Rules Comparison
#

ScenarioUse Value Receiver (r Type)Use Pointer Receiver (r *Type)
Field MutationNo (Cannot mutate).Yes (Mutates original fields).
Struct SizeSmall primitive structs.Heavy structs (Prevents copy memory overhead).
ConcurrencyThread-safe by value copy.Requires mutex locks if mutated concurrently.
ConsistencyIf any method needs pointer receiver, ALL methods should use pointer receivers!

4. Troubleshooting & Common Errors
#

Error 1: cannot call pointer method on value
#

The Cause: Attempting to call a pointer receiver method on an unaddressable value. The Fix: Store the struct in a variable or pass a pointer explicitly (&MyStruct{}).


Summary & Next Steps
#

In this episode:

  • We defined methods using Value and Pointer Receivers.
  • We analyzed when to use pointer receivers for mutation and performance.
  • We composed domain objects using Struct Embedding.

In Episode 9: Implicit Interfaces & Polymorphism, we will explore how Go implements duck typing with compile-time type safety!

go - This article is part of a series.
Part 8: This Article