Go - Composition
Concept: Embedding in Go does not create a subtyping relationship — a type that embeds another is not a subtype of the embedded type.
Why it matters: Misunderstanding embedding as inheritance leads to designs that break when Go's type system rejects substitution you expected to work.
package main
import "fmt"
type Engine struct{ HP int }
func (e Engine) Start() { fmt.Printf("engine %d HP starting\n", e.HP) }
type Car struct {
Engine // composition — not inheritance
Model string
}
// accepts Engine only — Car does NOT satisfy this parameter
func tune(e Engine) { fmt.Println("tuning engine", e.HP) }
func main() {
c := Car{Engine: Engine{HP: 200}, Model: "Sedan"}
c.Start() // promoted method — works
tune(c.Engine) // must extract the embedded value explicitly
// tune(c) // compile error: Car is not an Engine
}
Gotcha: Passing an outer type where the inner type is expected — embedding gives you promoted methods, not substitutability.
Concept: Focus on behavior ("what it does") rather than state ("what it is") when designing types and interfaces in Go.
Why it matters: Behavior-based design produces smaller interfaces, fewer dependencies, and types that are easier to test and replace.
package main
import "fmt"
// wrong: designed around what it IS (a database row)
type UserRow struct {
ID int
Name string
Email string
}
// right: designed around what it DOES — behavior is the contract
type UserStore interface {
FindByID(id int) (string, error) // behavior: can find a user
Save(name, email string) (int, error) // behavior: can persist a user
}
type MemUserStore struct{ users map[int]string }
func NewMemUserStore() *MemUserStore {
return &MemUserStore{users: make(map[int]string)}
}
func (s *MemUserStore) FindByID(id int) (string, error) {
u, ok := s.users[id]
if !ok {
return "", fmt.Errorf("user %d not found", id)
}
return u, nil
}
func (s *MemUserStore) Save(name, email string) (int, error) {
id := len(s.users) + 1
s.users[id] = name
return id, nil
}
func main() {
var store UserStore = NewMemUserStore()
id, _ := store.Save("Harish", "h@example.com")
name, _ := store.FindByID(id)
fmt.Println(name)
}
Gotcha: Designing interfaces around struct fields (getters/setters) instead of meaningful actions — the interface ends up exposing implementation rather than behavior.
Concept: Avoid unnecessary abstractions and prioritize duplication over the wrong abstraction.
Why it matters: A wrong abstraction is worse than duplication — it couples unrelated concepts and forces all implementations into a shape that fits none of them well.
package main
import "fmt"
// wrong abstraction: forced to unify two unrelated things under one interface
type Processable interface {
Process() error // Invoice and EmailNotification have nothing in common
}
// right: keep them separate — duplication is fine when concepts are unrelated
type Invoice struct{ amount float64 }
type EmailNotification struct{ recipient string }
func (inv Invoice) Generate() error {
fmt.Printf("invoice: $%.2f\n", inv.amount)
return nil
}
func (n EmailNotification) Send() error {
fmt.Printf("email to: %s\n", n.recipient)
return nil
}
func main() {
inv := Invoice{amount: 99.99}
notif := EmailNotification{recipient: "h@example.com"}
// explicit calls — no forced common abstraction
inv.Generate()
notif.Send()
}
Gotcha: Unifying two types under an interface just because they both have an error return — shared signature is not shared behavior.
Concept: Grouping types — organizing and categorizing data types based on shared behavior.
Why it matters: Grouping by behavior (interface) instead of type hierarchy produces extensible designs — new types plug in without modifying existing code.
package main
import "fmt"
// grouped by behavior: anything that can be serialized
type Serializable interface {
Serialize() string
}
type JSONPayload struct{ data string }
type CSVRow struct{ fields []string }
type BinaryBlob struct{ bytes []byte }
func (j JSONPayload) Serialize() string { return `{"data":"` + j.data + `"}` }
func (c CSVRow) Serialize() string {
result := ""
for i, f := range c.fields {
if i > 0 { result += "," }
result += f
}
return result
}
func (b BinaryBlob) Serialize() string { return fmt.Sprintf("blob[%d]", len(b.bytes)) }
func exportAll(items []Serializable) {
for _, item := range items {
fmt.Println(item.Serialize())
}
}
func main() {
exportAll([]Serializable{
JSONPayload{data: "hello"},
CSVRow{fields: []string{"a", "b", "c"}},
BinaryBlob{bytes: make([]byte, 8)},
})
}
Gotcha: Adding types to a behavior group that only partially implement the expected behavior — every method in the interface must be meaningful for every implementor.
Concept: Interface pollution — introducing unnecessary interfaces, leading to code complexity and reduced readability.
Why it matters: Every unnecessary interface adds a layer of indirection that forces readers to trace back to concrete types to understand what actually happens.
package main
import "fmt"
// polluted: interface exists for a single implementation — zero benefit
type EmailSenderInterface interface { // unnecessary
SendEmail(to, subject, body string) error
}
type EmailSender struct{ smtpHost string }
func (e EmailSender) SendEmail(to, subject, body string) error {
fmt.Printf("sending to %s via %s\n", to, e.smtpHost)
return nil
}
// clean: use concrete type directly — add interface only when a second impl appears
func notifyUser(sender EmailSender, email string) error {
return sender.SendEmail(email, "Welcome", "Hello!")
}
func main() {
sender := EmailSender{smtpHost: "smtp.example.com"}
notifyUser(sender, "user@example.com")
}
Gotcha: Defining an interface to "make testing easier" before writing any tests — define the interface in the test file when the need is proven.
Concept: Start with concrete implementations and discover interfaces through refactoring.
Why it matters: Interfaces discovered from real usage fit exactly — pre-designed interfaces almost always have the wrong shape or wrong granularity.
package main
import "fmt"
// step 1: start concrete — solve the problem first
type S3Storage struct{ bucket string }
func (s S3Storage) Upload(key string, data []byte) error {
fmt.Printf("s3://%s/%s (%d bytes)\n", s.bucket, key, len(data))
return nil
}
func (s S3Storage) Download(key string) ([]byte, error) {
return []byte("content"), nil
}
// step 2: second implementation appears — NOW extract the interface
type LocalStorage struct{ dir string }
func (l LocalStorage) Upload(key string, data []byte) error {
fmt.Printf("local://%s/%s (%d bytes)\n", l.dir, key, len(data))
return nil
}
func (l LocalStorage) Download(key string) ([]byte, error) {
return []byte("local content"), nil
}
// interface discovered from two real implementations — exact fit
type ObjectStorage interface {
Upload(key string, data []byte) error
Download(key string) ([]byte, error)
}
func backup(store ObjectStorage, key string, data []byte) error {
return store.Upload(key, data)
}
func main() {
backup(S3Storage{bucket: "prod"}, "db.sql", []byte("data"))
backup(LocalStorage{dir: "/tmp"}, "db.sql", []byte("data"))
}
Gotcha: Extracting an interface after only one implementation — you're guessing at the shape; wait for the second real implementation to confirm it.
Concept: Write code in testable layers, each focused on a specific aspect of the problem.
Why it matters: Layered code can be tested layer by layer in isolation — a single monolithic function can only be tested end-to-end.
package main
import "fmt"
// layer 1: pure data transformation — no I/O, fully testable
func parseCSVLine(line string) []string {
// simplified: real impl would handle quotes, escapes
result := []string{}
field := ""
for _, c := range line {
if c == ',' {
result = append(result, field)
field = ""
} else {
field += string(c)
}
}
return append(result, field)
}
// layer 2: business logic — depends on layer 1, testable with known inputs
func buildRecord(fields []string) (map[string]string, error) {
if len(fields) < 2 {
return nil, fmt.Errorf("buildRecord: need at least 2 fields")
}
return map[string]string{"name": fields[0], "email": fields[1]}, nil
}
// layer 3: orchestration — wires layers together; tested via integration test
func processLine(line string) (map[string]string, error) {
fields := parseCSVLine(line)
return buildRecord(fields)
}
func main() {
record, err := processLine("Harish,h@example.com")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(record)
}
Gotcha: Mixing I/O into business logic layers — a function that reads from a file AND transforms data can't be unit tested without a real file.
Concept: Decoupling allows for flexibility and adaptability to future changes by focusing on behavior rather than concrete types.
Why it matters: Decoupled code lets you swap implementations (real vs mock, v1 vs v2) without touching the business logic that uses them.
package main
import (
"fmt"
"time"
)
// behavior contract — business logic depends only on this
type Clock interface {
Now() time.Time
}
// production implementation
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }
// test implementation — controllable, deterministic
type FixedClock struct{ t time.Time }
func (f FixedClock) Now() time.Time { return f.t }
type Session struct {
clock Clock
createdAt time.Time
}
func NewSession(clock Clock) *Session {
return &Session{clock: clock, createdAt: clock.Now()}
}
func (s *Session) Age() time.Duration {
return s.clock.Now().Sub(s.createdAt)
}
func main() {
fixed := FixedClock{t: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}
sess := NewSession(fixed)
// deterministic in tests — no real time dependency
fmt.Println(sess.Age()) // 0s — created and checked at same fixed time
}
Gotcha: Injecting time.Now as a func() time.Time parameter instead of a Clock interface — function variables work but lose the ability to group related time operations.
Concept: Interface assignments involve copying the concrete data if type information is compatible.
Why it matters: Understanding what gets copied during interface assignment explains why value-type interface values are independent and pointer-type ones are shared.
package main
import "fmt"
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func (c Counter) Value() int { return c.n }
type Incrementer interface{ Inc() }
type Valuer interface{ Value() int }
func main() {
c := &Counter{}
// interface assignment: copies the pointer — both interface vars share same Counter
var inc Incrementer = c
var val Valuer = c
inc.Inc()
inc.Inc()
// val and inc both hold the same pointer — mutation is visible through val
fmt.Println(val.Value()) // 2 — shared state through pointer in interface
}
Gotcha: Assigning a value (not pointer) to an interface — the interface stores a copy of the value; mutations through one interface don't affect the original.
Concept: The compiler ensures type integrity during interface assignments — preventing assignments that violate the defined behavior.
Why it matters: Compile-time interface checking catches mismatches before any test runs — the compiler is your first and fastest test.
package main
import "fmt"
type Saver interface {
Save(data string) error
}
type Loader interface {
Load(id int) (string, error)
}
// compile-time assertions — zero runtime cost, immediate feedback
type FileSaver struct{ path string }
func (f FileSaver) Save(data string) error {
fmt.Printf("saving to %s\n", f.path)
return nil
}
// static interface check: if FileSaver stops implementing Saver, compile fails here
var _ Saver = FileSaver{}
// this would fail to compile if FileSaver doesn't implement Loader:
// var _ Loader = FileSaver{} // compile error: missing Load method
func main() {
var s Saver = FileSaver{path: "/tmp/data"}
s.Save("hello")
}
Gotcha: Only discovering interface satisfaction failure at the call site — use var _ Interface = Type{} assertions at the definition site to catch it immediately.
Concept: Go prioritizes compile-time checks for type compatibility, enhancing code safety and predictability.
Why it matters: Compile-time errors are free to fix; runtime panics in production cost real money and user trust.
package main
import "fmt"
type Celsius float64
type Fahrenheit float64
// compile-time type safety: wrong unit assignment is a compile error, not a runtime bug
func boilWater() Celsius { return 100 }
func bodyTemp() Fahrenheit { return 98.6 }
func isSafeToTouch(temp Celsius) bool {
return temp < 40
}
func main() {
c := boilWater()
f := bodyTemp()
fmt.Println(isSafeToTouch(c)) // false
// isSafeToTouch(f) // compile error: cannot use Fahrenheit as Celsius
// isSafeToTouch(float64(f)) // compile error: cannot use float64 as Celsius
isSafeToTouch(Celsius(f)) // explicit conversion — programmer acknowledges the risk
}
Gotcha: Using type Speed = float64 (alias) instead of type Speed float64 (distinct type) — aliases provide zero type safety.
Concept: Type assertions are runtime operations — errors are detected during program execution, not compile time.
Why it matters: Every type assertion that isn't protected by comma-ok is a potential production panic — they deserve the same care as error handling.
package main
import "fmt"
type Result struct{ value interface{} }
func (r Result) AsInt() (int, error) {
// runtime check: type unknown at compile time — must assert at runtime
v, ok := r.value.(int)
if !ok {
return 0, fmt.Errorf("AsInt: expected int, got %T", r.value)
}
return v, nil
}
func (r Result) AsString() (string, error) {
v, ok := r.value.(string)
if !ok {
return "", fmt.Errorf("AsString: expected string, got %T", r.value)
}
return v, nil
}
func main() {
results := []Result{{42}, {"hello"}, {3.14}}
for _, r := range results {
if v, err := r.AsInt(); err == nil {
fmt.Println("int:", v)
} else if s, err := r.AsString(); err == nil {
fmt.Println("string:", s)
} else {
fmt.Println("unknown:", r.value)
}
}
}
Gotcha: Catching a type assertion panic with recover as the primary error handling strategy — use comma-ok instead; recover is for truly unexpected failures.
Concept: The standard library uses type assertions to provide default implementations while allowing custom behavior.
Why it matters: This pattern — check for an optional interface, fall back to default — is how Go's standard library achieves extensibility without breaking changes.
package main
import "fmt"
// stdlib pattern: check for optional behavior, fall back to default
type Stringer interface{ String() string }
type User struct {
ID int
Name string
}
// User implements Stringer — fmt package checks for this interface
func (u User) String() string {
return fmt.Sprintf("User{id=%d name=%q}", u.ID, u.Name)
}
// mimic stdlib's optional interface check
func display(v interface{}) string {
if s, ok := v.(Stringer); ok {
return s.String() // custom representation
}
return fmt.Sprintf("%v", v) // default fallback
}
func main() {
u := User{ID: 1, Name: "Harish"}
i := 42
fmt.Println(display(u)) // uses User.String()
fmt.Println(display(i)) // fallback: 42
fmt.Println(u) // fmt automatically calls u.String()
}
Gotcha: Implementing String() string with a pointer receiver when passing values to fmt — fmt.Println(u) where u is a value won't call the pointer receiver's String().
Concept: Interface pollution leads to increased code complexity, reduced readability, and potential performance overhead due to unnecessary allocations.
Why it matters: Each unnecessary interface level adds an allocation + indirection on every call — measured cost, not theoretical.
package main
import (
"fmt"
"testing"
)
// polluted: interface with single impl adds alloc + indirection for no benefit
type NumberParser interface{ Parse(s string) (int, error) }
type defaultParser struct{}
func (defaultParser) Parse(s string) (int, error) { return 0, nil } // stub
// clean: function value or concrete type — no interface overhead
type Parser func(s string) (int, error)
func defaultParse(s string) (int, error) { return 0, nil } // stub
var sink int
func BenchmarkWithInterface(b *testing.B) {
var p NumberParser = defaultParser{}
for i := 0; i < b.N; i++ {
v, _ := p.Parse("42")
sink = v
}
}
func BenchmarkWithConcrete(b *testing.B) {
p := defaultParse
for i := 0; i < b.N; i++ {
v, _ := p("42")
sink = v
}
}
func main() { fmt.Println(defaultParse("42")) }
Gotcha: Accepting benchmark results showing "no difference" as proof the interface is free — the optimizer may have eliminated the dispatch; test with multiple concrete types.
Concept: Avoid designing with interfaces upfront — focus on concrete implementations and discover interfaces through refactoring as needed for decoupling.
Why it matters: Upfront interface design guesses at the right abstraction — concrete-first design reveals the actual abstraction through real usage patterns.
package main
import "fmt"
// phase 1: concrete, works, ships
type Notifier struct{ apiKey string }
func (n Notifier) Notify(msg string) error {
fmt.Printf("notify[%s]: %s\n", n.apiKey, msg)
return nil
}
// phase 2: requirement appears for SMS — NOW discover the interface
type SMSNotifier struct{ phone string }
func (s SMSNotifier) Notify(msg string) error {
fmt.Printf("sms[%s]: %s\n", s.phone, msg)
return nil
}
// interface discovered from two real types — minimum viable shape
type MessageNotifier interface {
Notify(msg string) error
}
func alertTeam(n MessageNotifier, msg string) {
if err := n.Notify(msg); err != nil {
fmt.Println("alert failed:", err)
}
}
func main() {
alertTeam(Notifier{apiKey: "abc"}, "deploy started")
alertTeam(SMSNotifier{phone: "+91..."}, "deploy done")
}
Gotcha: Designing the interface before writing either implementation — the interface shape is speculation and almost always needs changing once both types exist.
Concept: Question the use of interfaces when they don't provide clear and obvious benefits in terms of decoupling or supporting multiple implementations.
Why it matters: The three legitimate reasons for an interface: multiple implementations, testability, and reducing import cycles — if none apply, the interface is pollution.
package main
import "fmt"
// ask: does this interface have multiple implementations NOW or SOON?
// ask: does this interface help with testing?
// ask: does this interface break an import cycle?
// if all answers are NO: use the concrete type
type PDFExporter struct{ quality int }
func (p PDFExporter) Export(data string) error {
fmt.Printf("pdf[q=%d]: %s\n", p.quality, data)
return nil
}
// no interface needed — PDFExporter is used directly
// if a second exporter (CSV, HTML) appears, extract the interface then
func generateReport(exporter PDFExporter, title string) error {
return exporter.Export("Report: " + title)
}
func main() {
e := PDFExporter{quality: 300}
generateReport(e, "Q4 Results")
}
Gotcha: Adding an interface "in case we need it later" — YAGNI applies to interfaces; the refactoring cost when you do need it is low.
Concept: Mocking should be a last resort in testing, with data-oriented tests using concrete implementations preferred.
Why it matters: Tests against real implementations find real bugs; mocks test that your mock behaves like you expect your mock to behave — a tautology.
package main
import (
"fmt"
"strings"
)
// data-oriented test: pass real data through a real implementation
// no mock needed — the function is pure and deterministic
func processName(name string) string {
name = strings.TrimSpace(name)
name = strings.ToLower(name)
return strings.ReplaceAll(name, " ", "_")
}
// testable without any mock because it has no external dependencies
func main() {
cases := []struct{ in, want string }{
{" Harish S ", "harish_s"},
{"Alice Bob", "alice_bob"},
{"", ""},
}
for _, c := range cases {
got := processName(c.in)
if got != c.want {
fmt.Printf("FAIL: processName(%q) = %q, want %q\n", c.in, got, c.want)
} else {
fmt.Printf("PASS: %q → %q\n", c.in, got)
}
}
}
Gotcha: Mocking a function that has no I/O or external dependencies — if the function is pure, just call it with test data.
Concept: Go's approach to mocking allows API users to define their own interfaces for mocking purposes, freeing API developers from anticipating those needs.
Concept: Go's approach to mocking allows API users to define their own interfaces for mocking purposes, freeing API developers from anticipating those needs.
package main
import "fmt"
// library package: ships only the concrete type — no interface defined
type S3Client struct{ region string }
func (c S3Client) PutObject(bucket, key string, data []byte) error {
fmt.Printf("s3[%s] PUT %s/%s (%d bytes)\n", c.region, bucket, key, len(data))
return nil
}
func (c S3Client) GetObject(bucket, key string) ([]byte, error) {
return []byte("content"), nil
}
// consumer defines the interface they need — only the methods they actually call
type ObjectPutter interface {
PutObject(bucket, key string, data []byte) error
}
// mock defined in test code — not in the library
type MockPutter struct{ calls []string }
func (m *MockPutter) PutObject(bucket, key string, data []byte) error {
m.calls = append(m.calls, fmt.Sprintf("%s/%s", bucket, key))
return nil
}
func uploadBackup(putter ObjectPutter, data []byte) error {
return putter.PutObject("backups", "db.sql", data)
}
func main() {
// production
uploadBackup(S3Client{region: "ap-south-1"}, []byte("data"))
// test
mock := &MockPutter{}
uploadBackup(mock, []byte("data"))
fmt.Println("mock calls:", mock.calls)
}
Gotcha: Defining the mock interface in the library package — it creates a dependency from library to test infrastructure; define it in the consumer's package.
Concept: This approach promotes cleaner code and separation of concerns, allowing API users to tailor their testing strategies.
Why it matters: Separation of concerns means the library evolves independently of test infrastructure — adding a new method to S3Client doesn't break consumer mocks.
package main
import "fmt"
// narrow interfaces per use case — each consumer defines exactly what they need
type FileReader interface {
ReadFile(path string) ([]byte, error)
}
type FileWriter interface {
WriteFile(path string, data []byte) error
}
// consumer 1: only needs to read — mock is trivially simple
type MockReader struct{ data map[string][]byte }
func (m MockReader) ReadFile(path string) ([]byte, error) {
if d, ok := m.data[path]; ok {
return d, nil
}
return nil, fmt.Errorf("not found: %s", path)
}
// consumer 2: only needs to write — different mock, different concern
type MockWriter struct{ written map[string][]byte }
func (m *MockWriter) WriteFile(path string, data []byte) error {
m.written[path] = data
return nil
}
func readConfig(r FileReader) ([]byte, error) { return r.ReadFile("/etc/app.conf") }
func writeLog(w FileWriter, msg string) error { return w.WriteFile("/var/log/app.log", []byte(msg)) }
func main() {
reader := MockReader{data: map[string][]byte{"/etc/app.conf": []byte("port=8080")}}
data, _ := readConfig(reader)
fmt.Println(string(data))
writer := &MockWriter{written: make(map[string][]byte)}
writeLog(writer, "started")
fmt.Println(writer.written)
}
Gotcha: Defining one large FileSystem interface with all methods — consumers mock every method even if they only use one.