Go - Decoupling

Concept: Value Semantics — used for built-in types (int, string, bool) and for moving reference types (slices, maps, channels) around.

Why it matters: Built-in types have no shared state to protect — value semantics give safety and predictability with zero cost.

package main

import "fmt"

// value semantics for built-in types: safe, no synchronisation needed
func processMetrics(count int, label string, active bool, tags []string) {
	count++           // modifies local copy — caller's count untouched
	label += "_copy"  // new string — original unchanged
	active = !active  // flips local copy only
	tags = append(tags, "extra") // new backing array if cap exceeded — caller unaffected

	fmt.Println(count, label, active, tags)
}

func main() {
	c, l, a, t := 5, "metric", true, []string{"go", "backend"}
	processMetrics(c, l, a, t)
	fmt.Println(c, l, a, t) // unchanged: 5 metric true [go backend]
}

Gotcha: Passing a map by value expecting isolation — maps are reference types; the header is copied but mutations go through to the original.


Concept: Pointer Semantics — used for struct types to avoid unnecessary copying and handle data that can't be copied (e.g., mutexes).

Why it matters: A copied mutex is a broken mutex — types with internal synchronization must always be used via pointer semantics.

package main

import (
	"fmt"
	"sync"
)

// pointer semantics required: copying a Mutex is undefined behaviour
type SafeMap struct {
	mu   sync.Mutex         // must never be copied
	data map[string]int
}

func NewSafeMap() *SafeMap { // factory returns pointer — enforces pointer semantics
	return &SafeMap{data: make(map[string]int)}
}

func (m *SafeMap) Set(k string, v int) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.data[k] = v
}

func (m *SafeMap) Get(k string) (int, bool) {
	m.mu.Lock()
	defer m.mu.Unlock()
	v, ok := m.data[k]
	return v, ok
}

func main() {
	m := NewSafeMap()
	m.Set("requests", 42)
	v, _ := m.Get("requests")
	fmt.Println(v)
}

Gotcha: Assigning sm2 := *sm to copy a SafeMap — go vet catches this: "assignment copies lock value".


Concept: API Design Based on Data — the data's semantic dictates the API's semantic.

Why it matters: An API whose receiver semantic contradicts its data semantic forces callers into unsafe patterns.

package main

import "fmt"

// data is a struct with no lock — value semantics are appropriate
type Point struct{ X, Y float64 }

// API matches data semantic: value receiver returns new value — no mutation
func (p Point) Translate(dx, dy float64) Point {
	return Point{X: p.X + dx, Y: p.Y + dy}
}

// data has a counter that accumulates — pointer semantics are appropriate
type Accumulator struct{ total float64 }

// API matches data semantic: pointer receiver mutates in place
func (a *Accumulator) Add(v float64) { a.total += v }
func (a *Accumulator) Total() float64 { return a.total }

func main() {
	p := Point{1, 2}
	p2 := p.Translate(3, 4)
	fmt.Println(p, p2) // {1 2} {4 6} — original unchanged

	acc := &Accumulator{}
	acc.Add(10)
	acc.Add(20)
	fmt.Println(acc.Total()) // 30
}

Gotcha: Using a pointer receiver on a type like Point where value semantics are natural — callers can't chain calls cleanly and must manage pointer lifetimes.


Concept: Key Principle — acceptable to go from value to pointer semantics in limited scopes, but never from pointer to value semantics.

Why it matters: Copying a value that is being shared via pointer can produce a divergent copy — changes to one are invisible to the other, silently breaking invariants.

package main

import "fmt"

type Node struct {
	Value int
	Next  *Node
}

// value → pointer: acceptable in a limited scope (just this function)
func initNode(v int) *Node {
	n := Node{Value: v}   // created as value
	return &n             // address taken — escapes to heap — ok, limited scope
}

// pointer → value: NEVER do this with shared data
func dangerousCopy(n *Node) Node {
	return *n // creates an independent copy — mutations to the copy are invisible
}

func main() {
	n := initNode(42)
	n.Value = 99

	copy := dangerousCopy(n) // copy is now diverged from n
	copy.Value = 0
	fmt.Println(n.Value)    // 99 — original unchanged, copy diverged silently
	fmt.Println(copy.Value) // 0
}

Gotcha: Dereferencing a shared pointer into a local variable to "work safely" — mutations to the local never propagate back to shared state.


Concept: Data Semantics Drive Behavior and Cost — understanding value or pointer semantic allows predicting code behavior and associated costs.

Why it matters: Every method call has a predictable cost once you know the semantic — value means copy, pointer means indirection and potential heap allocation.

package main

import (
	"fmt"
	"unsafe"
)

type SmallStruct struct{ X, Y int }     // 16 bytes — value copy is cheap
type LargeStruct struct{ data [1024]int } // 8KB — value copy is expensive

// cheap: 16 bytes copied on every call
func processSmall(s SmallStruct) int { return s.X + s.Y }

// cheap: 8 bytes (pointer) copied — large struct stays in place
func processLarge(s *LargeStruct) int { return s.data[0] + s.data[1023] }

func main() {
	small := SmallStruct{1, 2}
	large := &LargeStruct{}
	large.data[0], large.data[1023] = 5, 10

	fmt.Println(processSmall(small))  // 16 bytes copied
	fmt.Println(processLarge(large))  // 8 bytes copied (pointer)

	fmt.Printf("SmallStruct size: %d bytes\n", unsafe.Sizeof(small))
	fmt.Printf("LargeStruct size: %d bytes\n", unsafe.Sizeof(*large))
}

Gotcha: Using pointer semantics for small value types to "save copies" — the indirection and potential cache miss costs more than the copy.


Concept: Decoupling always incurs the cost of allocation and indirection regardless of the data semantic.

Why it matters: Every interface assignment stores data on the heap and adds a pointer indirection — decoupling is never free; choose it deliberately.

package main

import (
	"fmt"
	"testing"
)

type Hasher interface{ Hash(s string) uint64 }

type FNVHasher struct{}

func (FNVHasher) Hash(s string) uint64 {
	var h uint64 = 14695981039346656037
	for i := 0; i < len(s); i++ {
		h ^= uint64(s[i])
		h *= 1099511628211
	}
	return h
}

// direct call: inlineable, no allocation
func directHash(h FNVHasher, s string) uint64 { return h.Hash(s) }

// decoupled call: interface allocation + indirect call through ITable
func decoupledHash(h Hasher, s string) uint64 { return h.Hash(s) }

var sink uint64

func BenchmarkDirect(b *testing.B) {
	h := FNVHasher{}
	for i := 0; i < b.N; i++ { sink = directHash(h, "hello") }
}

func BenchmarkDecoupled(b *testing.B) {
	var h Hasher = FNVHasher{}
	for i := 0; i < b.N; i++ { sink = decoupledHash(h, "hello") }
}

func main() { fmt.Println(directHash(FNVHasher{}, "hello")) }

Gotcha: Benchmarking interface dispatch without -benchmem — the allocation cost is invisible without the -benchmem flag.


Concept: Decoupling is a necessary trade-off for maintainability and flexibility — be aware of the costs.

Why it matters: The right question is not "should I decouple?" but "does the maintainability benefit justify the allocation cost at this call frequency?"

package main

import "fmt"

// high-frequency path: concrete type — no decoupling, maximum performance
type DirectLogger struct{}

func (DirectLogger) Log(msg string) { fmt.Println(msg) }

// low-frequency path: interface — decoupling justified, cost is negligible
type Logger interface{ Log(msg string) }

type FileLogger struct{ path string }
type StdoutLogger struct{}

func (f FileLogger) Log(msg string)  { fmt.Printf("[%s] %s\n", f.path, msg) }
func (StdoutLogger) Log(msg string)  { fmt.Println(msg) }

// startup config: called once — interface cost is irrelevant
func configureLogging(env string) Logger {
	if env == "prod" {
		return FileLogger{path: "/var/log/app.log"}
	}
	return StdoutLogger{}
}

func main() {
	log := configureLogging("dev")
	log.Log("server started")  // interface call: fine for low-frequency startup
}

Gotcha: Decoupling every struct in a codebase by default — reserve interfaces for genuine variation points, not speculative future changes.


Concept: Avoiding Getters and Setters — Go discourages them; design APIs that offer valuable actions or data transformations.

Why it matters: Getters/setters add boilerplate without adding behavior — they expose implementation details through a thin wrapper that provides no real encapsulation.

package main

import (
	"errors"
	"fmt"
)

// wrong: getter/setter pattern — exposes implementation, provides no invariant
type BadAccount struct{ balance float64 }
func (a *BadAccount) GetBalance() float64    { return a.balance }
func (a *BadAccount) SetBalance(v float64)   { a.balance = v } // no validation!

// right: behavior-based API — invariants enforced, meaningful actions
type Account struct{ balance float64 }

func (a *Account) Deposit(amount float64) error {
	if amount <= 0 {
		return errors.New("deposit: amount must be positive")
	}
	a.balance += amount
	return nil
}

func (a *Account) Withdraw(amount float64) error {
	if amount > a.balance {
		return errors.New("withdraw: insufficient funds")
	}
	a.balance -= amount
	return nil
}

func (a *Account) Balance() float64 { return a.balance } // read-only — ok

func main() {
	acc := &Account{}
	acc.Deposit(100)
	acc.Withdraw(30)
	fmt.Printf("balance: %.2f\n", acc.Balance()) // 70.00
}

Gotcha: Naming a read-only accessor GetBalance() — Go convention is Balance() without the Get prefix.


Concept: Method Calls as Syntactic Sugar — method calls are function calls with the receiver as the first parameter.

Why it matters: Understanding this equivalence clarifies how method values, method expressions, and interface dispatch all work under the hood.

package main

import "fmt"

type Greeter struct{ name string }

func (g Greeter) Hello() string { return "Hello, " + g.name }

func main() {
	g := Greeter{name: "Harish"}

	// syntactic sugar form
	fmt.Println(g.Hello())

	// method expression: type as receiver — identical behavior
	fmt.Println(Greeter.Hello(g))

	// method value: bound to a specific receiver
	hello := g.Hello        // hello is a func() string with g captured
	fmt.Println(hello())    // same as g.Hello()

	// function variable: store method for later call
	var fn func() string = g.Hello
	fmt.Println(fn())
}

Gotcha: Storing a method value from a value receiver — the method value captures a copy of the receiver at the time of binding, not a live reference.


Concept: Polymorphism — the ability of a piece of code to change its behavior based on the concrete data type it's operating on.

Why it matters: Polymorphism is the mechanism that allows one function to work correctly with many different types — the foundation of extensible design.

package main

import (
	"fmt"
	"math"
)

type Shape interface{ Area() float64 }

type Circle struct{ R float64 }
type Square struct{ S float64 }
type Triangle struct{ B, H float64 }

func (c Circle) Area() float64   { return math.Pi * c.R * c.R }
func (s Square) Area() float64   { return s.S * s.S }
func (t Triangle) Area() float64 { return 0.5 * t.B * t.H }

// one function — three different behaviors — decided at runtime
func totalArea(shapes []Shape) float64 {
	total := 0.0
	for _, s := range shapes {
		total += s.Area() // polymorphic dispatch through ITable
	}
	return total
}

func main() {
	shapes := []Shape{
		Circle{R: 5},
		Square{S: 4},
		Triangle{B: 6, H: 3},
	}
	fmt.Printf("total area: %.2f\n", totalArea(shapes))
}

Gotcha: Using a type switch instead of polymorphism for behavior dispatch — adding a new type requires modifying the switch, breaking the open/closed principle.


Concept: Interfaces are not concrete types and hold no data themselves — they define a method set, a contract of behavior.

Why it matters: An interface variable is a two-word value (type pointer + data pointer) — it stores a reference to the concrete data, never the data itself.

package main

import (
	"fmt"
	"unsafe"
)

type Stringer interface{ String() string }

type Name struct{ first, last string }

func (n Name) String() string { return n.first + " " + n.last }

func main() {
	var s Stringer // zero value: both words nil

	n := Name{"Harish", "S"}
	s = n // interface now holds: type=*itab(Name,Stringer), data=ptr to copy of n

	fmt.Println(s.String())
	fmt.Printf("interface header size: %d bytes\n", unsafe.Sizeof(&s)) // 8 (pointer)
	fmt.Printf("interface value size:  %d bytes\n", unsafe.Sizeof(s))  // 16 (2 words)
}

Gotcha: Assigning a nil pointer of a concrete type to an interface — the interface is non-nil (has a type word) even though the data pointer is nil.


Concept: Interface Implementation — a concrete type implements an interface by having methods matching the method set; no explicit declaration needed.

Why it matters: Implicit implementation means interfaces can be defined by the consumer, not the producer — this inverts the dependency and avoids package coupling.

package main

import (
	"fmt"
	"strings"
)

// interface defined in the consumer's package — producer knows nothing about it
type Transformer interface {
	Transform(s string) string
}

// stdlib types implement our interface implicitly — no modification needed
type UpperTransformer struct{}
type TrimTransformer struct{}

func (UpperTransformer) Transform(s string) string { return strings.ToUpper(s) }
func (TrimTransformer) Transform(s string) string  { return strings.TrimSpace(s) }

func applyAll(transformers []Transformer, input string) string {
	for _, t := range transformers {
		input = t.Transform(input)
	}
	return input
}

func main() {
	result := applyAll(
		[]Transformer{TrimTransformer{}, UpperTransformer{}},
		"  hello world  ",
	)
	fmt.Println(result) // HELLO WORLD
}

Gotcha: Defining an interface in the same package as its only implementation — the interface adds indirection cost without enabling any decoupling.


Concept: Polymorphic Functions — functions that accept interface types as parameters can operate on any concrete type implementing the interface.

Why it matters: Polymorphic functions are the primary mechanism for writing code that works with future types that don't exist yet.

package main

import (
	"fmt"
	"io"
	"os"
	"strings"
)

// io.Writer is the canonical polymorphic interface — write to anything
func writeReport(w io.Writer, title string, lines []string) error {
	if _, err := fmt.Fprintf(w, "=== %s ===\n", title); err != nil {
		return err
	}
	for _, line := range lines {
		if _, err := fmt.Fprintln(w, line); err != nil {
			return err
		}
	}
	return nil
}

func main() {
	lines := []string{"total: 100", "errors: 0", "latency: 5ms"}

	// same function writes to stdout, string buffer, or file
	writeReport(os.Stdout, "Metrics", lines)

	var buf strings.Builder
	writeReport(&buf, "Metrics", lines)
	fmt.Println("captured:", len(buf.String()), "bytes")

	// would also work with: f, _ := os.Create("report.txt"); writeReport(f, ...)
	_ = os.Stdout
}

Gotcha: Making every function parameter an interface "for flexibility" — use concrete types by default; accept interfaces only when multiple implementations are expected.


Concept: Decoupling through Interfaces — requesting data based on behavior rather than specific type enables decoupling.

Why it matters: Depending on behavior (interface) instead of identity (concrete type) means implementations can change without modifying call sites.

package main

import "fmt"

// tightly coupled: depends on PostgresDB specifically
type PostgresDB struct{}
func (p PostgresDB) QueryUser(id int) string { return fmt.Sprintf("user-%d", id) }

// decoupled: depends only on the behavior needed
type UserQuerier interface {
	QueryUser(id int) string
}

type UserService struct{ db UserQuerier }

func NewUserService(db UserQuerier) *UserService { return &UserService{db: db} }

func (s *UserService) GetUser(id int) string {
	return s.db.QueryUser(id) // works with any UserQuerier
}

// swap implementation without touching UserService
type MockDB struct{}
func (m MockDB) QueryUser(id int) string { return "mock-user" }

func main() {
	svc := NewUserService(PostgresDB{})
	fmt.Println(svc.GetUser(1)) // user-1

	mockSvc := NewUserService(MockDB{})
	fmt.Println(mockSvc.GetUser(1)) // mock-user — zero code change in UserService
}

Gotcha: Defining the interface in the same package as UserService and importing it from PostgresDB — this inverts the dependency correctly only if the interface lives with the consumer.


Concept: ITable (Interface Table) — the mechanism Go uses to dispatch interface method calls at runtime.

Why it matters: Every interface call goes through an ITable lookup — understanding this explains the constant (not proportional) overhead of interface dispatch.

package main

import "fmt"

type Animal interface{ Sound() string }

type Dog struct{}
type Cat struct{}

func (Dog) Sound() string { return "woof" }
func (Cat) Sound() string { return "meow" }

func makeSound(a Animal) string {
	// runtime: load ITable from interface word 1
	//          look up Sound method pointer in ITable
	//          call through pointer — one extra indirection vs direct call
	return a.Sound()
}

func main() {
	// ITable for Dog/Animal is built once at program init — not per call
	animals := []Animal{Dog{}, Cat{}, Dog{}}
	for _, a := range animals {
		fmt.Println(makeSound(a))
	}
}

Gotcha: Benchmarking interface dispatch with a single concrete type — the CPU branch predictor learns the pattern and predicts the ITable lookup, inflating performance vs real-world polymorphism.


Concept: Method Sets — value receivers attach methods to values; pointer receivers attach methods to both values and pointers.

Why it matters: A value type can only use value-receiver methods; a pointer type can use both — this asymmetry determines interface satisfaction.

package main

import "fmt"

type Printer interface{ Print() }

type Doc struct{ content string }

// value receiver: part of both Doc and *Doc method sets
func (d Doc) Print() { fmt.Println(d.content) }

// pointer receiver: part of *Doc method set ONLY
func (d *Doc) Append(s string) { d.content += s }

func printDoc(p Printer) { p.Print() }

func main() {
	d := Doc{content: "hello"}

	// both Doc and *Doc satisfy Printer (Print has value receiver)
	printDoc(d)   // ok: Doc satisfies Printer
	printDoc(&d)  // ok: *Doc satisfies Printer

	// only *Doc can call Append
	d.Append(" world") // Go auto-takes address when variable is addressable
	d.Print()

	// printDoc(Doc{content: "x"}) with a pointer-only interface would fail:
	// if Printer had Append() too, only *Doc would satisfy it
}

Gotcha: Passing a non-addressable value (e.g., function return) to a function expecting an interface when the concrete type only has pointer receivers — compile error.


Concept: Data Integrity through Method Sets — Go's method set rules prevent mixing pointer and value semantics inappropriately.

Why it matters: The compiler enforces that you don't accidentally copy a type that was designed for pointer semantics — catching potential data corruption at compile time.

package main

import (
	"fmt"
	"sync"
)

type SafeCounter interface {
	Inc()
	Value() int
}

// pointer receivers only: copying this type would break the mutex
type counter struct {
	mu    sync.Mutex
	count int
}

func (c *counter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.count++
}

func (c *counter) Value() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}

func increment(sc SafeCounter, n int) {
	for i := 0; i < n; i++ {
		sc.Inc() // only *counter satisfies SafeCounter — value copy impossible
	}
}

func main() {
	c := &counter{}
	increment(c, 100)
	fmt.Println(c.Value()) // 100

	// var sc SafeCounter = counter{} // compile error — counter doesn't satisfy SafeCounter
}

Gotcha: Storing a counter value in an interface — Go requires the pointer because pointer receivers don't promote to the value's method set.


Concept: Pointer Semantics Imply Sharing — if interface implemented using pointer semantics, only shared data (pointers) can be passed.

Why it matters: This rule reinforces the principle that shared data must not be copied — the type system prevents accidental snapshot copies of shared state.

package main

import (
	"fmt"
	"sync"
)

type Repository interface {
	Save(id int, val string)
	Load(id int) string
}

// implemented with pointer receiver — shared mutable state
type InMemRepo struct {
	mu   sync.Mutex
	data map[int]string
}

func NewInMemRepo() *InMemRepo {
	return &InMemRepo{data: make(map[int]string)}
}

func (r *InMemRepo) Save(id int, val string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.data[id] = val
}

func (r *InMemRepo) Load(id int) string {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.data[id]
}

func persist(repo Repository) {
	repo.Save(1, "Harish") // pointer semantic: mutation visible everywhere
}

func main() {
	repo := NewInMemRepo()
	persist(repo)
	fmt.Println(repo.Load(1)) // Harish — shared state was modified
}

Gotcha: Passing *InMemRepo by value to satisfy Repository — not possible; value of type InMemRepo doesn't have pointer receiver methods.


Concept: Value Semantics Encourage Copying — copies are generally used when interacting with value-semantic interfaces.

Why it matters: Value-semantic interface implementations are safe to copy — each caller gets an independent snapshot with no shared state risk.

package main

import "fmt"

type Formatter interface{ Format(s string) string }

// value receiver: Formatter implemented with value semantics — safe to copy
type PrefixFormatter struct{ prefix string }

func (f PrefixFormatter) Format(s string) string {
	return f.prefix + ": " + s
}

func applyFormat(f Formatter, messages []string) []string {
	out := make([]string, len(messages))
	for i, m := range messages {
		out[i] = f.Format(m)
	}
	return out
}

func main() {
	f := PrefixFormatter{prefix: "INFO"}
	msgs := []string{"started", "processing", "done"}

	// passing value: a copy of f is stored in the interface — independent
	result := applyFormat(f, msgs)
	fmt.Println(result)

	f.prefix = "DEBUG" // mutating f doesn't affect the copy inside applyFormat
}

Gotcha: Using value semantics on a type that accumulates state (like a counter) — each copy starts fresh, silently losing all accumulated data.


Concept: Compiler as a Safeguard — the Go compiler enforces method set rules, preventing code that could violate data integrity.

Why it matters: Compile-time enforcement of semantic rules eliminates an entire class of runtime bugs — no test can catch what the compiler already rejects.

package main

import "fmt"

type Writer interface{ Write(p []byte) (int, error) }

type BufWriter struct{ buf []byte }

// pointer receiver: BufWriter only satisfies Writer as *BufWriter
func (w *BufWriter) Write(p []byte) (int, error) {
	w.buf = append(w.buf, p...)
	return len(p), nil
}

// compile-time assertion: will fail to compile if *BufWriter doesn't satisfy Writer
var _ Writer = (*BufWriter)(nil) // zero-cost check — evaluated at compile time only

func writeAll(w Writer, data [][]byte) {
	for _, d := range data {
		w.Write(d)
	}
}

func main() {
	bw := &BufWriter{}
	writeAll(bw, [][]byte{[]byte("hello"), []byte(" world")})
	fmt.Println(string(bw.buf))
}

Gotcha: Relying on a test to catch interface satisfaction failures — use the var _ Interface = (*Type)(nil) compile-time assertion instead.


Concept: Collections Based on Behavior — interfaces allow creation of collections holding different concrete types unified by shared behavior.

Why it matters: Behavior-based collections let you process heterogeneous types with a single loop — no type switches, no conditional logic.

package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Circle struct{ R float64 }
type Rect struct{ W, H float64 }

func (c Circle) Area() float64      { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }
func (r Rect) Area() float64        { return r.W * r.H }
func (r Rect) Perimeter() float64   { return 2 * (r.W + r.H) }

func summary(shapes []Shape) {
	for _, s := range shapes {
		fmt.Printf("%T  area=%.2f  perimeter=%.2f\n", s, s.Area(), s.Perimeter())
	}
}

func main() {
	shapes := []Shape{
		Circle{R: 3},
		Rect{W: 4, H: 5},
		Circle{R: 1},
	}
	summary(shapes)
}

Gotcha: Using []interface{} instead of a typed interface slice — you lose compile-time method safety and every access requires a type assertion.


Concept: Iterating over Interface Collections — correct for range form depends on underlying collection type and how the interface is used.

Why it matters: Choosing the wrong range form when the collection is modified inside the loop causes silent logic bugs or panics.

package main

import "fmt"

type Handler interface{ Handle(event string) }

type LogHandler struct{ name string }
type MetricHandler struct{ name string }

func (h LogHandler) Handle(event string)    { fmt.Printf("[log:%s] %s\n", h.name, event) }
func (h MetricHandler) Handle(event string) { fmt.Printf("[metric:%s] %s\n", h.name, event) }

func dispatch(handlers []Handler, event string) {
	// value semantic range: correct — handlers slice is not modified inside loop
	for _, h := range handlers {
		h.Handle(event) // h is a copy of the interface value — safe
	}
}

func main() {
	handlers := []Handler{
		LogHandler{name: "audit"},
		MetricHandler{name: "latency"},
		LogHandler{name: "error"},
	}
	dispatch(handlers, "user.login")
}

Gotcha: Modifying the handlers slice inside the loop (adding/removing handlers) while using value-semantic range — the loop's snapshot won't see the changes.


Concept: Understanding Behavior in Collections — knowing data semantics of concrete types is crucial for understanding behavior when modifying or accessing elements.

Why it matters: An interface slice hides whether each element is a value type or pointer type — you must know the concrete semantic to predict mutation behavior.

package main

import "fmt"

type Resetter interface{ Reset() }

type ValueCounter struct{ count int }  // value semantics
type PtrCounter struct{ count int }    // pointer semantics

func (c *ValueCounter) Reset() { c.count = 0 } // pointer receiver — must store *ValueCounter
func (c *PtrCounter) Reset()   { c.count = 0 }

func resetAll(resetters []Resetter) {
	for _, r := range resetters {
		r.Reset() // mutations go through pointer — visible on the original
	}
}

func main() {
	vc := &ValueCounter{count: 5}
	pc := &PtrCounter{count: 10}

	resetters := []Resetter{vc, pc}
	resetAll(resetters)

	fmt.Println(vc.count) // 0 — pointer in interface, mutation propagated
	fmt.Println(pc.count) // 0 — same
}

Gotcha: Storing a value (not pointer) in an interface — mutations in the method operate on the interface's internal copy, not the original variable.


Concept: Empty Interface (interface{}) — can hold any value regardless of its type; doesn't enforce any methods.

Why it matters: interface{} (or any in modern Go) is the escape hatch from the type system — use it only when the type is genuinely unknown at compile time.

package main

import "fmt"

// legitimate use: serialization where type is unknown until runtime
func serialize(values map[string]interface{}) string {
	result := "{"
	for k, v := range values {
		result += fmt.Sprintf(`"%s":%v,`, k, v)
	}
	return result + "}"
}

func main() {
	data := map[string]interface{}{
		"name":    "Harish",
		"age":     35,
		"active":  true,
		"score":   9.5,
	}
	fmt.Println(serialize(data))

	// modern Go: 'any' is an alias for interface{}
	var x any = 42
	fmt.Printf("type=%T value=%v\n", x, x)
}

Gotcha: Using interface{} for function parameters when the function only needs one or two behaviors — define a minimal interface instead.


Concept: Type Assertion Syntax — uses the form value.(type) where value is the interface variable.

Why it matters: Type assertions extract the concrete value from behind the interface — they are runtime operations with a definite cost and failure mode.

package main

import "fmt"

func printLength(v interface{}) {
	// direct assertion: panics if wrong type — only use when type is guaranteed
	s := v.(string)
	fmt.Println("length:", len(s))
}

func safePrintLength(v interface{}) {
	// comma-ok form: handles mismatch gracefully — always prefer this
	s, ok := v.(string)
	if !ok {
		fmt.Printf("expected string, got %T\n", v)
		return
	}
	fmt.Println("length:", len(s))
}

func main() {
	safePrintLength("hello")    // length: 5
	safePrintLength(42)          // expected string, got int

	// direct assertion — only safe when you know the type
	var i interface{} = "world"
	fmt.Println(i.(string))
}

Gotcha: Using the direct (non-comma-ok) assertion in library code — any unexpected type causes an unrecoverable panic at the call site.


Concept: Handling Type Assertion Failures — using the comma-ok idiom or type switch to safely handle mismatches.

Why it matters: An unhandled type assertion failure panics the entire goroutine — always use comma-ok or type switch unless you are 100% certain of the type.

package main

import "fmt"

type Measurement struct{ value float64 }

func process(v interface{}) (float64, error) {
	// type switch: handles multiple types cleanly — no nested if/ok chains
	switch val := v.(type) {
	case float64:
		return val, nil
	case int:
		return float64(val), nil
	case Measurement:
		return val.value, nil
	default:
		return 0, fmt.Errorf("process: unsupported type %T", v)
	}
}

func main() {
	inputs := []interface{}{3.14, 42, Measurement{9.81}, "oops"}
	for _, input := range inputs {
		result, err := process(input)
		if err != nil {
			fmt.Println("error:", err)
			continue
		}
		fmt.Printf("%.4f\n", result)
	}
}

Gotcha: Adding a default case to a type switch but silently ignoring it — always return an error or log the unexpected type.


Concept: Caution with Empty Interface — overuse can make code harder to reason about and indicate design issues.

Why it matters: Every interface{} at a call site requires the reader to mentally track what type is actually there — it transfers cognitive load from the compiler to the human.

package main

import "fmt"

// wrong: uses interface{} when a concrete type is known
func addWrong(a, b interface{}) interface{} {
	return a.(int) + b.(int) // panics on wrong type — no compile-time safety
}

// right: concrete types — compiler validates, no runtime surprise
func addRight(a, b int) int { return a + b }

// right: when type genuinely varies, use a typed interface
type Adder interface{ Add(other Adder) Adder }

func main() {
	fmt.Println(addRight(3, 4)) // safe, clear, fast

	// interface{} only when necessary — e.g., fmt.Println accepts any value
	fmt.Println("mixed:", 1, "two", 3.0, true)
}

Gotcha: Returning interface{} from a public API function — callers must assert the type on every use, spreading assertion code and panic risk throughout the codebase.


Concept: Embedding — involves placing a struct type directly within another without a field name; the embedded type becomes an "inner type".

Why it matters: Embedding promotes the inner type's API to the outer type without writing delegation boilerplate — composition without inheritance.

package main

import "fmt"

type Logger struct{ prefix string }

func (l Logger) Log(msg string) {
	fmt.Printf("[%s] %s\n", l.prefix, msg)
}

// Server embeds Logger — gets Log method promoted automatically
type Server struct {
	Logger        // inner type: all methods promoted to Server
	host   string
	port   int
}

func main() {
	s := Server{
		Logger: Logger{prefix: "SERVER"},
		host:   "localhost",
		port:   8080,
	}

	s.Log("started")             // promoted method — called on embedded Logger
	s.Logger.Log("also works")   // explicit inner type access
	fmt.Println(s.prefix)        // promoted field access
}

Gotcha: Embedding a type and also defining a method with the same name — the outer type's method silently shadows the promoted one; the inner one is still reachable via s.Inner.Method().


Concept: Inner Type Promotion — all fields and methods of the inner type become part of the outer type's public interface.

Why it matters: Promotion means the outer type satisfies any interface the inner type satisfies — without writing any forwarding methods.

package main

import (
	"fmt"
	"io"
	"strings"
)

// outer type automatically satisfies io.Reader through promotion
type TrackedReader struct {
	*strings.Reader        // inner type — Read method promoted
	bytesRead int
}

func NewTrackedReader(s string) *TrackedReader {
	return &TrackedReader{Reader: strings.NewReader(s)}
}

func (tr *TrackedReader) Read(p []byte) (int, error) {
	n, err := tr.Reader.Read(p) // delegate to inner type
	tr.bytesRead += n
	return n, err
}

func readAll(r io.Reader) string {
	buf := make([]byte, 64)
	var result []byte
	for {
		n, err := r.Read(buf)
		result = append(result, buf[:n]...)
		if err == io.EOF { break }
	}
	return string(result)
}

func main() {
	tr := NewTrackedReader("hello world")
	content := readAll(tr)
	fmt.Printf("content: %q  bytes tracked: %d\n", content, tr.bytesRead)
}

Gotcha: Forgetting that promotion also exposes unexported fields indirectly through promoted exported methods — review what the outer type's API surface becomes.


Concept: Accessing Inner Type Members — can be done directly using the promoted names or explicitly using the type name.

Why it matters: Both access paths work — the explicit path is essential when the outer type overrides a promoted method and you need the original.

package main

import "fmt"

type Base struct {
	ID   int
	Name string
}

func (b Base) Describe() string {
	return fmt.Sprintf("Base{ID:%d Name:%s}", b.ID, b.Name)
}

type Extended struct {
	Base          // inner type
	Extra string
}

func main() {
	e := Extended{
		Base:  Base{ID: 1, Name: "widget"},
		Extra: "bonus",
	}

	// promoted access — reads as if fields/methods are on Extended directly
	fmt.Println(e.ID)         // promoted field
	fmt.Println(e.Name)       // promoted field
	fmt.Println(e.Describe()) // promoted method

	// explicit inner type access — identical result, clearer origin
	fmt.Println(e.Base.ID)
	fmt.Println(e.Base.Describe())
}

Gotcha: Accessing promoted fields in a test to verify inner state — it tightly couples the test to the embedding structure; prefer methods that expose the needed behavior.


Concept: Overriding Promoted Members — outer type can define methods with same names; outer type's implementation takes precedence.

Why it matters: Overriding lets you extend or replace inner behavior without changing the inner type — the key pattern for decorator and middleware composition.

package main

import "fmt"

type BaseHandler struct{}

func (BaseHandler) ServeHTTP(path string) string {
	return "base: " + path
}

// Middleware overrides ServeHTTP — wraps inner behavior
type LoggingHandler struct {
	BaseHandler        // inner type
}

func (h LoggingHandler) ServeHTTP(path string) string {
	result := h.BaseHandler.ServeHTTP(path) // call inner explicitly
	fmt.Printf("LOG: %s → %s\n", path, result)
	return result
}

func main() {
	base := BaseHandler{}
	logging := LoggingHandler{BaseHandler: base}

	fmt.Println(base.ServeHTTP("/api/users"))    // base handler
	fmt.Println(logging.ServeHTTP("/api/users")) // override: logs then delegates
}

Gotcha: Forgetting to call the inner type's method in the override — the behavior of the base type is silently lost.


Concept: No Subtyping — the outer type is not a subtype of the inner type; embedding is composition, not inheritance.

Why it matters: You cannot substitute an Extended for a Base — they are different types with no is-a relationship; only interface implementation creates substitutability.

package main

import "fmt"

type Animal struct{ Name string }

func (a Animal) Breathe() { fmt.Println(a.Name, "breathes") }

type Dog struct {
	Animal
	Breed string
}

// accepts Animal — Dog does NOT satisfy this; there is no subtype relationship
func care(a Animal) { a.Breathe() }

func main() {
	d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Lab"}
	d.Breathe() // promoted method — works

	// care(d) // compile error: cannot use Dog as Animal — no subtyping in Go

	care(d.Animal) // must explicitly pass the embedded Animal value
}

Gotcha: Expecting embedding to work like inheritance for function arguments — it doesn't; you must extract the embedded value or define an interface.


Concept: Avoiding Deeply Nested Embedding and Multiple Embedding with Same Method Names.

Why it matters: Deep or ambiguous embedding makes it impossible to predict which method gets called — the compiler will error on ambiguous promotion.

package main

import "fmt"

type A struct{}
type B struct{}
type C struct{ A; B } // embeds both A and B

func (A) Hello() string { return "hello from A" }
func (B) Hello() string { return "hello from B" }

func main() {
	c := C{}

	// ambiguous: both A and B promote Hello — compiler rejects c.Hello()
	// fmt.Println(c.Hello()) // compile error: ambiguous selector c.Hello

	// must disambiguate explicitly
	fmt.Println(c.A.Hello()) // explicit: A's Hello
	fmt.Println(c.B.Hello()) // explicit: B's Hello
}

Gotcha: Adding a second embedded type to a struct and not checking for method name conflicts — silent promotion ambiguity becomes a compile error in all call sites.


Concept: Packages as Static Libraries — each folder in a Go project represents a package treated as a static library by the compiler.

Why it matters: Treating packages as API units forces you to design explicit boundaries and public contracts rather than treating all code as one big namespace.

// Package calc provides pure arithmetic operations with no external dependencies.
// It is designed to be importable by any package without policy side effects.
package calc

// Add returns the sum of a and b.
func Add(a, b int) int { return a + b }

// Sub returns the difference of a and b.
func Sub(a, b int) int { return a - b }

// Mul returns the product of a and b.
func Mul(a, b int) int { return a * b }

// Div returns a/b and an error if b is zero.
func Div(a, b int) (int, error) {
	if b == 0 {
		return 0, fmt.Errorf("calc.Div: division by zero")
	}
	return a / b, nil
}

Gotcha: Putting all code in one package to "avoid imports" — you lose the encapsulation, testability, and API clarity that package boundaries provide.


Concept: Exporting Based on Capitalization — identifier starting with capital letter is exported; lowercase is only accessible within the same package.

Why it matters: Capitalization-based visibility is the simplest possible encapsulation mechanism — no keywords, no annotations, instantly readable.

package store

import "fmt"

// exported: public API — accessible from any package
type Record struct {
	ID   int    // exported field
	Data string // exported field
	hash string // unexported field — internal implementation detail
}

// exported: part of public API
func New(id int, data string) Record {
	return Record{
		ID:   id,
		Data: data,
		hash: fmt.Sprintf("%d:%s", id, data), // computed internally
	}
}

// unexported: internal helper — not part of public API
func computeHash(r Record) string {
	return r.hash // accessible within same package
}

Gotcha: Exporting a field that should be read-only — any external package can now mutate it; use an unexported field with an exported accessor instead.


Concept: Accessing Exported Identifiers — code in other packages uses packageName.ExportedType

Why it matters: The package name becomes the namespace — it disambiguates types across packages and makes the origin of every identifier explicit at the call site.

package main

import (
	"fmt"
	"strings"
	"time"
)

func main() {
	// package name as namespace — origin is unambiguous
	now := time.Now()                          // time.Now — from "time" package
	upper := strings.ToUpper("hello")          // strings.ToUpper — from "strings"
	dur := time.Since(now)                     // time.Since — same package, different func

	fmt.Println(upper, dur)

	// naming collision resolved by package prefix
	// time.Time vs your own Time type — always clear which is which
	var t time.Time = time.Now()
	fmt.Println(t.Format(time.RFC3339))
}

Gotcha: Dot-importing a package (import . "pkg") to avoid the prefix — it pollutes the namespace and makes it impossible to tell where identifiers come from.


Concept: Encapsulation and Name Access — exporting controls name access, not direct data access; factory functions return unexported types.

Why it matters: Returning an unexported type from an exported factory forces all construction through the factory — invariants can never be bypassed.

package token

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"time"
)

// unexported type — callers can't construct this directly
type authToken struct {
	value     string
	expiresAt time.Time
}

// exported method on unexported type — accessible via the interface
func (t authToken) Value() string       { return t.value }
func (t authToken) Expired() bool       { return time.Now().After(t.expiresAt) }
func (t authToken) String() string      { return fmt.Sprintf("token(%s)", t.value[:8]) }

// exported factory — only valid construction path
func New(ttl time.Duration) (authToken, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return authToken{}, fmt.Errorf("token.New: %w", err)
	}
	return authToken{
		value:     hex.EncodeToString(b),
		expiresAt: time.Now().Add(ttl),
	}, nil
}

Gotcha: Returning a pointer to an unexported type — callers can still store and pass it, but they can't create one without the factory; this is intentional.


Concept: Exported and Unexported Fields — fields within a struct follow the same capitalization rule.

Why it matters: Unexported fields enforce invariants — callers cannot put the struct in an invalid state by directly writing to internal fields.

package main

import "fmt"

type Queue struct {
	items    []int // unexported — callers can't bypass Enqueue/Dequeue
	capacity int   // unexported — fixed at construction
}

func NewQueue(cap int) *Queue {
	return &Queue{
		items:    make([]int, 0, cap),
		capacity: cap,
	}
}

func (q *Queue) Enqueue(v int) bool {
	if len(q.items) >= q.capacity {
		return false // invariant: never exceed capacity
	}
	q.items = append(q.items, v)
	return true
}

func (q *Queue) Dequeue() (int, bool) {
	if len(q.items) == 0 {
		return 0, false
	}
	v := q.items[0]
	q.items = q.items[1:]
	return v, true
}

func (q *Queue) Len() int { return len(q.items) }

func main() {
	q := NewQueue(3)
	q.Enqueue(1); q.Enqueue(2); q.Enqueue(3)
	fmt.Println(q.Enqueue(4)) // false — capacity enforced
	v, _ := q.Dequeue()
	fmt.Println(v, q.Len())   // 1, 2
}

Gotcha: Using json:"items" on an unexported field — the JSON encoder ignores unexported fields; you must export them or implement MarshalJSON.


Concept: Partial Construction — avoid patterns where a struct with exported fields embeds an unexported type.

Why it matters: Partial construction creates structs that callers can only half-initialize — the unexported embedded type is invisible and unset, silently producing broken state.

package main

import "fmt"

// wrong: exported struct embeds unexported type — caller can't set inner
type badConfig struct {
	inner    unexportedInner // caller outside this package can never set this
	MaxConns int
}

type unexportedInner struct{ timeout int }

// right: all construction goes through a factory — no partial state possible
type Config struct {
	maxConns int
	timeout  int
}

func NewConfig(maxConns, timeoutSec int) Config {
	return Config{maxConns: maxConns, timeout: timeoutSec}
}

func (c Config) MaxConns() int  { return c.maxConns }
func (c Config) Timeout() int   { return c.timeout }

func main() {
	cfg := NewConfig(10, 30)
	fmt.Println(cfg.MaxConns(), cfg.Timeout())
}

Gotcha: Making all fields exported "for convenience" in marshaling scenarios — use struct tags with an exported type instead of partially-constructible hybrids.


Concept: Common Use of Unexported Types with Exported Fields — used for marshaling where fields need to be exported for reflection but type should remain internal.

Why it matters: JSON/encoding packages use reflection which can only see exported fields — the type itself stays unexported to prevent external construction.

package config

import (
	"encoding/json"
	"fmt"
)

// unexported type: callers cannot construct directly — must use Load()
type settings struct {
	Host     string `json:"host"`     // exported field: reflection can see it
	Port     int    `json:"port"`     // exported field: JSON works correctly
	Debug    bool   `json:"debug"`
}

// Settings is the exported interface callers use
type Settings interface {
	Host() string
	Port() int
	Debug() bool
}

func (s settings) Host() string { return s.Host }
func (s settings) Port() int    { return s.Port }
func (s settings) Debug() bool  { return s.Debug }

func Load(data []byte) (Settings, error) {
	var s settings
	if err := json.Unmarshal(data, &s); err != nil {
		return nil, fmt.Errorf("config.Load: %w", err)
	}
	return s, nil
}

Gotcha: Naming the exported fields and the accessor methods identically — Go will complain about method names conflicting with field names; use different casing or names.