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.