Go - Packaging

Concept: Packaging in Go diverges from the norm — folders represent distinct APIs, forming the fundamental units for compilation.

Why it matters: Treating each folder as an API unit forces deliberate boundary design — you can't accidentally use unexported details across packages.

// myapp/internal/invoice/invoice.go
// Each folder is a compilation unit — this package has one job: invoice logic
package invoice

import "fmt"

type Invoice struct {
	ID     int
	Amount float64
}

// Exported API: the only surface callers can use
func New(id int, amount float64) Invoice {
	return Invoice{ID: id, Amount: amount}
}

func (inv Invoice) String() string {
	return fmt.Sprintf("INV-%04d $%.2f", inv.ID, inv.Amount)
}

Gotcha: Naming a package utils or common — a package without a single clear purpose becomes a dumping ground that everything imports, making circular imports inevitable.


Concept: Folders in a Go project represent distinct APIs — not simply containers for source code.

Why it matters: Each package is an independently compilable and testable unit — designing folders as APIs forces you to think about what belongs together.

// myapp/internal/pricing/pricing.go
// package pricing owns one concern: price calculation rules
package pricing

// Pricer computes the final price for a quantity — the entire public API
type Pricer struct{ unitPrice float64 }

func New(unitPrice float64) Pricer { return Pricer{unitPrice: unitPrice} }

func (p Pricer) Calculate(qty int) float64 {
	price := p.unitPrice * float64(qty)
	if qty >= 10 {
		price *= 0.9 // 10% bulk discount
	}
	return price
}
// myapp/cmd/main.go
// cmd imports internal packages — top-level orchestration only
package main

import (
	"fmt"
	// "myapp/internal/invoice"
	// "myapp/internal/pricing"
)

func main() {
	// each import is a distinct API — dependencies are explicit and visible
	fmt.Println("wiring packages together at the cmd layer")
}

Gotcha: Putting business logic in main.gomain should only wire things together; all logic should live in importable packages.


Concept: No subpackaging in Go — hierarchical folder layout doesn't imply parent-child relationships between packages.

Why it matters: internal/user and internal/user/profile are completely independent packages — there is no inheritance of visibility or behavior between them.

// internal/user/user.go — package user
package user

type User struct {
	ID   int
	Name string
}

func Find(id int) (User, error) {
	return User{ID: id, Name: "Harish"}, nil
}
// internal/user/profile/profile.go — package profile (NOT a child of user)
package profile

// profile CANNOT access unexported identifiers from package user
// it must import user and use only its exported API
import "fmt"

type Profile struct {
	UserID  int    // profile stores user ID by value — no direct coupling
	Bio     string
	Website string
}

func New(userID int, bio string) Profile {
	return Profile{UserID: userID, Bio: bio}
}

func (p Profile) String() string {
	return fmt.Sprintf("profile[user=%d]: %s", p.UserID, p.Bio)
}

Gotcha: Expecting internal/user/profile to have access to user's unexported types — it doesn't; they are siblings, not parent-child.


Concept: Bidirectional dependencies are prohibited — two packages cannot import each other.

Why it matters: Circular imports are a compile error in Go — this forces you to design clean dependency graphs where data flows in one direction.

// internal/order/order.go
// order depends on product — one-way dependency
package order

// import "myapp/internal/product" // ok: order → product

type Order struct {
	ID        int
	ProductID int
	Qty       int
}

func New(id, productID, qty int) Order {
	return Order{ID: id, ProductID: productID, Qty: qty}
}
// internal/product/product.go
// product must NOT import order — that would create a cycle
package product

// import "myapp/internal/order" // COMPILE ERROR: import cycle

type Product struct {
	ID    int
	Name  string
	Price float64
}

func New(id int, name string, price float64) Product {
	return Product{ID: id, Name: name, Price: price}
}

Gotcha: Solving a circular import by moving code into a third "common" package — if both packages still import that common package, the design is still entangled; reconsider the abstraction boundary.


Concept: Package-oriented design — central role of packages in structuring a Go application.

Why it matters: Package-oriented design produces codebases where each package can be understood, tested, and replaced independently — the foundation of maintainable large systems.

// platform/postgres/postgres.go
// platform: foundational packages — no business logic, no policy decisions
package postgres

import "fmt"

type DB struct {
	host string
	port int
}

func New(host string, port int) *DB {
	return &DB{host: host, port: port}
}

func (db *DB) Ping() error {
	fmt.Printf("ping %s:%d\n", db.host, db.port)
	return nil
}

func (db *DB) Query(q string) ([]map[string]interface{}, error) {
	fmt.Printf("query: %s\n", q)
	return nil, nil
}

Gotcha: Putting database schema migrations in the postgres platform package — migrations are policy (they change the business schema) and belong in internal/.


Concept: Purposeful packages — a well-designed package should have a clear singular purpose.

Why it matters: A package with one purpose has one reason to change — it's independently testable, importable without side effects, and understandable in isolation.

// kit/validate/validate.go
// one purpose: input validation rules — nothing else
package validate

import (
	"fmt"
	"regexp"
)

var emailRE = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

// Email returns an error if the address is not valid
func Email(addr string) error {
	if !emailRE.MatchString(addr) {
		return fmt.Errorf("validate.Email: %q is not a valid email", addr)
	}
	return nil
}

// NonEmpty returns an error if s is empty or whitespace-only
func NonEmpty(field, s string) error {
	if len(s) == 0 {
		return fmt.Errorf("validate.NonEmpty: %s is required", field)
	}
	return nil
}

Gotcha: Adding HTTP handlers or database calls to a validation package — the package now has multiple reasons to change and can no longer be imported without those dependencies.


Concept: API organization — Go leverages folders to organize APIs; each folder represents a distinct API.

Why it matters: Folder = package = API boundary means you can review the entire API of a package by looking at its exported identifiers — no separate documentation needed.

// internal/auth/auth.go
// The auth package API: all exported identifiers form the public contract
package auth

import (
	"errors"
	"fmt"
	"time"
)

// exported types — part of the API
type Token struct {
	Value     string
	ExpiresAt time.Time
}

// exported errors — part of the API
var (
	ErrExpired  = errors.New("token expired")
	ErrInvalid  = errors.New("token invalid")
)

// exported functions — part of the API
func Issue(subject string, ttl time.Duration) Token {
	return Token{
		Value:     fmt.Sprintf("tok-%s-%d", subject, time.Now().UnixNano()),
		ExpiresAt: time.Now().Add(ttl),
	}
}

func Validate(tok Token) error {
	if tok.Value == "" {
		return ErrInvalid
	}
	if time.Now().After(tok.ExpiresAt) {
		return ErrExpired
	}
	return nil
}

Gotcha: Scattering auth logic across multiple packages — the caller must import several packages to perform one logical operation, obscuring the API shape.


Concept: Firewalls between code — strict boundaries enforced by Go's packaging system limit visibility between packages.

Why it matters: Package boundaries are compile-enforced firewalls — a change inside a package cannot break code outside it as long as the exported API is preserved.

// internal/cache/cache.go
// The cache internals are completely hidden — callers see only the exported API
package cache

import "sync"

// unexported: callers can't access or depend on this implementation detail
type entry struct {
	value interface{}
	hits  int
}

type Cache struct {
	mu    sync.RWMutex
	store map[string]entry // unexported field — implementation firewall
}

func New() *Cache { return &Cache{store: make(map[string]entry)} }

// exported API: stable contract that hides the internal entry struct
func (c *Cache) Set(key string, value interface{}) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.store[key] = entry{value: value}
}

func (c *Cache) Get(key string) (interface{}, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	e, ok := c.store[key]
	if !ok {
		return nil, false
	}
	return e.value, true
}

Gotcha: Changing the entry struct's fields — safe, because callers can't reference entry; but changing the Get signature breaks all callers.


Concept: Exporting and unexporting — capitalization controls visibility between packages.

Why it matters: Every exported identifier is a commitment — it becomes part of your API contract and breaking it breaks all callers across all versions.

// internal/metric/metric.go
package metric

import (
	"fmt"
	"sync"
	"time"
)

// exported: part of the public contract
type Counter struct {
	name string        // unexported: implementation detail
	mu   sync.Mutex    // unexported: must never be copied
	n    int64         // unexported: accessed only via Inc/Value
}

func NewCounter(name string) *Counter { return &Counter{name: name} }

func (c *Counter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n++
}

func (c *Counter) Value() int64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.n
}

// unexported helper: not part of the public API
func (c *Counter) reset() { c.n = 0 }

// Exported: used in reports
func (c *Counter) String() string {
	return fmt.Sprintf("%s=%d @%s", c.name, c.Value(), time.Now().Format(time.RFC3339))
}

Gotcha: Exporting a method that was previously unexported — it's a non-breaking change; unexporting a previously exported method is a breaking change.


Concept: Import paths — each package identified by a unique import path reflecting the directory structure.

Why it matters: Import paths are the unique identity of a package — they prevent naming collisions and enable reproducible builds with Go modules.

// go.mod — module declaration defines the import path root
// module github.com/shisodeharish/treasury-data-hub
// go 1.21

// internal/ingestion/ingestion.go
package ingestion

import (
	"fmt"
	// full import path: module + relative directory path
	// "github.com/shisodeharish/treasury-data-hub/platform/postgres"
	// "github.com/shisodeharish/treasury-data-hub/kit/logger"
)

type Pipeline struct {
	name string
}

func New(name string) *Pipeline { return &Pipeline{name: name} }

func (p *Pipeline) Run() error {
	fmt.Printf("pipeline %s: running\n", p.name)
	return nil
}

Gotcha: Using relative import paths like ../postgres — Go modules require full module-relative import paths; relative paths only work within workspace mode.


Concept: Dependency management — preventing circular imports and using Go modules to manage external dependencies.

Why it matters: Go modules provide a reproducible, versioned dependency graph — go.sum ensures you always build with exactly the same code.

// go.mod example — defines module identity and dependency versions
// module github.com/shisodeharish/myapp
//
// go 1.21
//
// require (
//     github.com/lib/pq v1.10.9
//     github.com/redis/go-redis/v9 v9.3.0
// )

package main

import "fmt"

// dependency graph: cmd → internal → platform → (external modules)
// direction is always downward — never upward, never circular

func main() {
	fmt.Println("dependency direction: cmd → internal → platform → external")
	// go mod tidy    — add/remove dependencies
	// go mod verify  — check integrity against go.sum
	// go mod graph   — visualize the dependency tree
}

Gotcha: Editing go.sum manually — it is a machine-generated integrity file; run go mod tidy to regenerate it.


Concept: Usability and portability — a package should be intuitive and minimize external dependencies.

Why it matters: A package with fewer dependencies is easier to import, test, and use in different contexts — dependency sprawl is a form of technical debt.

// kit/conv/conv.go
// portable: zero external dependencies — uses only stdlib
package conv

import "strconv"

// IntToStr converts int to string — simple, zero-dependency utility
func IntToStr(n int) string { return strconv.Itoa(n) }

// StrToInt converts string to int, returns 0 on failure — safe default
func StrToInt(s string) int {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0
	}
	return n
}

// FloatToStr formats a float64 to 2 decimal places
func FloatToStr(f float64) string { return strconv.FormatFloat(f, 'f', 2, 64) }

Gotcha: Importing a heavy framework (e.g., a full ORM) in a foundational package — every package that imports it inherits the entire transitive dependency tree.


Concept: Policy constraints — design decisions in a package can impose constraints on packages that depend on it.

Why it matters: A foundational package that hard-codes a logging library forces every consumer to use that logger — policy decisions in foundations become mandatory everywhere.

// platform/postgres/postgres.go
// policy-free: accepts a logger interface — does not impose a logging library choice
package postgres

import "fmt"

// Logger interface: accepts any logger — no policy imposed on consumers
type Logger interface {
	Info(msg string)
	Error(msg string)
}

// noopLogger: default when caller doesn't provide one — no forced dependency
type noopLogger struct{}
func (noopLogger) Info(msg string)  {}
func (noopLogger) Error(msg string) {}

type DB struct {
	host string
	log  Logger
}

func New(host string, opts ...func(*DB)) *DB {
	db := &DB{host: host, log: noopLogger{}}
	for _, opt := range opts {
		opt(db)
	}
	return db
}

// WithLogger is an option — consumer chooses which logger to inject
func WithLogger(l Logger) func(*DB) {
	return func(db *DB) { db.log = l }
}

func (db *DB) Ping() error {
	db.log.Info(fmt.Sprintf("ping %s", db.host))
	return nil
}

Gotcha: Using log.Printf directly in platform packages — this hard-codes the stdlib logger format and destination, removing control from the consumer.


Concept: A layered project structure promotes modularity and maintainability — "kit", "application" with cmd/internal/platform/vendor.

Why it matters: The layered structure creates a dependency hierarchy that flows one way — cmd → internal → platform → kit, preventing tangled imports.

// Canonical project structure:
//
// myapp/
// ├── cmd/
// │   └── api/
// │       └── main.go       ← wires everything; imports internal only
// ├── internal/
// │   ├── user/
// │   │   └── user.go       ← business logic; imports platform, kit
// │   └── order/
// │       └── order.go      ← business logic; imports platform, kit
// ├── platform/
// │   ├── postgres/          ← infrastructure; imports kit only
// │   └── redis/
// ├── kit/
// │   ├── logger/            ← foundational; imports stdlib only
// │   └── validate/
// └── vendor/                ← pinned external deps (optional)

package main

import "fmt"

func main() {
	fmt.Println("dependency flow: cmd → internal → platform → kit → stdlib")
	// Validate with: go mod graph | grep "myapp"
}

Gotcha: Importing internal/ packages from platform/ — this reverses the dependency flow and entangles infrastructure with business logic.


Concept: The "kit" project acts as a customized standard library shared across multiple applications.

Why it matters: A kit package extracts proven, reusable code from applications into a portable library — preventing duplication across multiple services without coupling their business logic.

// kit/logger/logger.go
// kit: portable, no business logic, no external dependencies beyond stdlib
package logger

import (
	"fmt"
	"log"
	"os"
	"time"
)

type Level int

const (
	LevelDebug Level = iota
	LevelInfo
	LevelError
)

type Logger struct {
	level  Level
	prefix string
	l      *log.Logger
}

func New(prefix string, level Level) *Logger {
	return &Logger{
		level:  level,
		prefix: prefix,
		l:      log.New(os.Stdout, "", 0),
	}
}

func (lg *Logger) Info(msg string) {
	if lg.level <= LevelInfo {
		lg.l.Printf("%s [INFO] [%s] %s\n", time.Now().Format(time.RFC3339), lg.prefix, msg)
	}
}

func (lg *Logger) Error(msg string) {
	lg.l.Printf("%s [ERROR] [%s] %s\n", time.Now().Format(time.RFC3339), lg.prefix, msg)
}

func (lg *Logger) Debug(msg string) {
	if lg.level <= LevelDebug {
		lg.l.Printf("%s [DEBUG] [%s] %s\n", time.Now().Format(time.RFC3339), lg.prefix, msg)
	}
}

func main() {
	lg := New("api", LevelInfo)
	lg.Info("server started")
	lg.Debug("this won't print at Info level")
	fmt.Println("kit logger ready")
}

Gotcha: Adding business domain types to the kit package — kit must remain domain-agnostic; business types create unwanted coupling between services.


Concept: The "internal" folder safeguards business logic — the internal designation prevents external project imports.

Why it matters: Go's internal directory restriction is enforced by the compiler — any package outside the module root cannot import from internal/, enforcing business logic encapsulation.

// internal/subscription/subscription.go
// protected by Go's internal package rule — unreachable from external modules
package subscription

import (
	"errors"
	"fmt"
	"time"
)

var ErrAlreadyActive = errors.New("subscription already active")

type Plan string

const (
	PlanFree    Plan = "free"
	PlanPro     Plan = "pro"
	PlanEnterprise Plan = "enterprise"
)

type Subscription struct {
	UserID    int
	Plan      Plan
	StartedAt time.Time
	active    bool
}

func New(userID int, plan Plan) (*Subscription, error) {
	return &Subscription{
		UserID:    userID,
		Plan:      plan,
		StartedAt: time.Now(),
		active:    true,
	}, nil
}

func (s *Subscription) Upgrade(newPlan Plan) error {
	if !s.active {
		return fmt.Errorf("upgrade: subscription not active")
	}
	s.Plan = newPlan
	return nil
}

func (s *Subscription) Cancel() { s.active = false }
func (s *Subscription) Active() bool { return s.active }

Gotcha: Placing a genuinely reusable utility inside internal/ — it can't be imported by other modules; move it to kit/ if it should be shared.


Concept: The "platform" folder encapsulates project-specific foundations — infrastructure concerns tailored to the project.

Why it matters: Platform packages separate infrastructure mechanics from business logic — swapping PostgreSQL for CockroachDB requires changing only the platform layer.

// platform/queues/sqs.go
// platform: infrastructure adapter — no business logic, wraps AWS SQS
package queues

import "fmt"

type Message struct {
	ID   string
	Body string
}

type SQSQueue struct {
	url    string
	region string
}

func NewSQSQueue(url, region string) *SQSQueue {
	return &SQSQueue{url: url, region: region}
}

func (q *SQSQueue) Send(msg Message) error {
	fmt.Printf("sqs[%s] send: %s\n", q.url, msg.Body)
	return nil
}

func (q *SQSQueue) Receive() ([]Message, error) {
	fmt.Printf("sqs[%s] receive\n", q.url)
	return []Message{{ID: "1", Body: "event-data"}}, nil
}

func (q *SQSQueue) Delete(msgID string) error {
	fmt.Printf("sqs[%s] delete: %s\n", q.url, msgID)
	return nil
}

Gotcha: Putting retry logic and circuit breakers inside platform packages — these are cross-cutting concerns; they belong in kit/ or as middleware wrappers.


Concept: The "cmd" folder holds executable entry points — application's executable programs.

Why it matters: Separating cmd/ from internal/ means the business logic is importable as a library by other programs — main is just the wiring, not the logic.

// cmd/api/main.go — entry point: wires, configures, starts
package main

import (
	"fmt"
	"net/http"
	"os"
	"time"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	// wiring: cmd creates and connects packages — no business logic here
	mux := http.NewServeMux()

	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintln(w, `{"status":"ok"}`)
	})

	srv := &http.Server{
		Addr:         ":" + port,
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	fmt.Println("listening on", srv.Addr)
	if err := srv.ListenAndServe(); err != nil {
		fmt.Fprintln(os.Stderr, "server error:", err)
		os.Exit(1)
	}
}

Gotcha: Adding business rules to main.go — they can't be tested without spinning up the entire program; move them to internal/.


Concept: Dependency direction and validation — enforcing downward dependency flow prevents circular dependencies.

Why it matters: One-directional dependency flow makes the codebase a directed acyclic graph — every package can be compiled and tested independently.

// Dependency direction: cmd → internal → platform → kit → stdlib
// Validate with: go build ./...  (circular imports = compile error)

// internal/report/report.go — may import platform, kit; must NOT import cmd
package report

import "fmt"

// this package sits in internal — it can go DOWN to platform/kit
// it must never import UP to cmd

type Report struct {
	Title  string
	Lines  []string
}

func New(title string) *Report {
	return &Report{Title: title}
}

func (r *Report) AddLine(line string) {
	r.Lines = append(r.Lines, line)
}

func (r *Report) Render() string {
	out := fmt.Sprintf("=== %s ===\n", r.Title)
	for _, l := range r.Lines {
		out += l + "\n"
	}
	return out
}

Gotcha: Importing cmd/ packages from internal/ to access configuration — extract the configuration into a kit/config package that both layers can import.


Concept: Policy setting and constraints — technology choices (database, logging) in foundational packages impact portability.

Why it matters: A foundation that bakes in a specific logger or database forces every consumer to accept that dependency — reducing portability and testability.

// kit/store/store.go
// policy-free store interface — implementations live in platform/
package store

// Store defines behavior — no policy about which database is used
type Store interface {
	Get(key string) ([]byte, error)
	Set(key string, value []byte) error
	Delete(key string) error
}

// MemStore: zero-dependency implementation for tests and local development
type MemStore struct{ data map[string][]byte }

func NewMemStore() *MemStore { return &MemStore{data: make(map[string][]byte)} }

func (m *MemStore) Get(key string) ([]byte, error) {
	v, ok := m.data[key]
	if !ok {
		return nil, nil
	}
	return v, nil
}

func (m *MemStore) Set(key string, value []byte) error {
	m.data[key] = value
	return nil
}

func (m *MemStore) Delete(key string) error {
	delete(m.data, key)
	return nil
}

Gotcha: Returning Redis-specific error types from the Store interface — callers become coupled to Redis; return your own error types from kit/store.


Concept: Data semantics and consistency — consistent use of value and pointer semantics for data types is essential for code clarity.

Why it matters: Inconsistent semantics across a package force callers to check whether each function mutates or copies — consistent patterns eliminate that cognitive load.

// internal/account/account.go
// consistent: all mutating operations use pointer receivers — value operations use value receivers
package account

import (
	"errors"
	"fmt"
)

var ErrInsufficientFunds = errors.New("insufficient funds")

type Account struct {
	id      string
	balance float64
}

func New(id string, initialBalance float64) Account {
	return Account{id: id, balance: initialBalance} // value construction
}

// pointer receivers: mutate state — consistent semantic signal
func (a *Account) Deposit(amount float64) error {
	if amount <= 0 {
		return fmt.Errorf("deposit: amount must be positive")
	}
	a.balance += amount
	return nil
}

func (a *Account) Withdraw(amount float64) error {
	if amount > a.balance {
		return fmt.Errorf("withdraw: %w", ErrInsufficientFunds)
	}
	a.balance -= amount
	return nil
}

// value receiver: read-only query — no mutation
func (a Account) Balance() float64 { return a.balance }
func (a Account) ID() string       { return a.id }

Gotcha: Mixing value and pointer receivers on the same type — if any receiver mutates state, use pointer receivers throughout for consistency.


Concept: Error handling and recovery — robust error handling, logging, error propagation, and recovery strategies.

Why it matters: Production systems face unexpected failures — structured error propagation with recovery boundaries ensures one failure doesn't bring down the entire service.

package main

import (
	"fmt"
	"log"
	"net/http"
)

// recovery middleware: catches panics in HTTP handlers — service stays alive
func recoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				// log with enough context to diagnose the panic
				log.Printf("PANIC recovered: %v | method=%s path=%s",
					rec, r.Method, r.URL.Path)
				http.Error(w, "internal server error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func panickyHandler(w http.ResponseWriter, r *http.Request) {
	panic("simulated handler panic")
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/panic", panickyHandler)

	handler := recoveryMiddleware(mux)
	fmt.Println("server with recovery middleware")

	// http.ListenAndServe(":8080", handler)
	// simulate a request
	_ = handler
}

Gotcha: Using recover() to swallow panics without logging — the panic is hidden, the bug is never fixed, and the system silently degrades.


Concept: Testing strategies — unit tests, integration tests, and end-to-end tests organized by the project structure.

Why it matters: The package structure determines what can be tested in isolation — well-structured packages enable fast unit tests alongside slower integration tests.

// internal/pricing/pricing_test.go
// unit test: tests pricing logic in isolation — no database, no network
package pricing

import "testing"

type Pricer struct{ unitPrice float64 }
func NewPricer(p float64) Pricer { return Pricer{unitPrice: p} }
func (p Pricer) Calculate(qty int) float64 {
	price := p.unitPrice * float64(qty)
	if qty >= 10 { price *= 0.9 }
	return price
}

func TestCalculate_BulkDiscount(t *testing.T) {
	p := NewPricer(10.0)

	tests := []struct {
		qty  int
		want float64
	}{
		{1, 10.0},   // no discount
		{9, 90.0},   // no discount
		{10, 90.0},  // 10% discount: 100 * 0.9
		{20, 180.0}, // 10% discount: 200 * 0.9
	}

	for _, tc := range tests {
		got := p.Calculate(tc.qty)
		if got != tc.want {
			t.Errorf("Calculate(%d) = %.2f, want %.2f", tc.qty, got, tc.want)
		}
	}
}

Gotcha: Writing tests in package pricing_test (external) when you need to test unexported behavior — use package pricing (internal test) to access unexported identifiers.


Concept: Concurrency management — Go's concurrency primitives require careful management to avoid data races and deadlocks.

Why it matters: Data races produce non-deterministic bugs that are hard to reproduce — the race detector and structured concurrent patterns eliminate them.

package main

import (
	"fmt"
	"sync"
)

// structured concurrency: clear ownership — one goroutine owns each data structure
type SafeQueue struct {
	mu    sync.Mutex
	items []string
}

func (q *SafeQueue) Enqueue(item string) {
	q.mu.Lock()
	defer q.mu.Unlock()
	q.items = append(q.items, item)
}

func (q *SafeQueue) Dequeue() (string, bool) {
	q.mu.Lock()
	defer q.mu.Unlock()
	if len(q.items) == 0 {
		return "", false
	}
	item := q.items[0]
	q.items = q.items[1:]
	return item, true
}

func main() {
	q := &SafeQueue{}
	var wg sync.WaitGroup

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			q.Enqueue(fmt.Sprintf("item-%d", n))
		}(i)
	}
	wg.Wait()

	for {
		item, ok := q.Dequeue()
		if !ok { break }
		fmt.Println(item)
	}
}
// Run with: go run -race main.go

Gotcha: Using sync.Mutex and sync.RWMutex interchangeably — RWMutex is only better when reads dominate; under write-heavy loads it performs worse.


Concept: Code readability and maintainability — consistent coding conventions, meaningful names, and clear concise code.

Why it matters: Code that reads like English reduces the time to understand, modify, and review — the biggest productivity multiplier in a team.

package main

import (
	"fmt"
	"time"
)

// before: cryptic abbreviations, unclear intent
func proc(d []int, ts time.Time) (int, bool) {
	var r int
	for _, v := range d {
		if v > 0 { r += v }
	}
	return r, r > 0
}

// after: meaningful names, clear intent — reads like a specification
func sumPositiveValues(values []int, asOf time.Time) (total int, hasValues bool) {
	for _, v := range values {
		if v > 0 {
			total += v
		}
	}
	return total, total > 0
}

func main() {
	values := []int{-1, 2, -3, 4, 5}
	total, ok := sumPositiveValues(values, time.Now())
	fmt.Printf("total=%d hasValues=%v\n", total, ok)
}

Gotcha: Using excessively long names like totalSumOfAllPositiveIntegerValuesInTheSlice — Go convention is concise but clear; context reduces the name length needed.


Concept: Mental models for project organization — a well-defined project structure helps developers build accurate mental models.

Why it matters: When new developers can predict where to find code based on the project structure, onboarding time drops from weeks to days.

// mental model encoded as directory structure:
//
// Where does authentication logic live?  → internal/auth/
// Where does database code live?         → platform/postgres/
// Where does the HTTP server start?      → cmd/api/main.go
// Where does shared validation live?     → kit/validate/
// Where do integration tests live?       → internal/*/..._integration_test.go

package main

import "fmt"

func main() {
	// the structure IS the documentation — no wiki needed
	structure := map[string]string{
		"cmd/":      "entry points — wire and start",
		"internal/": "business logic — protected from external import",
		"platform/": "infrastructure adapters — databases, queues, caches",
		"kit/":      "shared foundational utilities — portable, no business logic",
	}

	for dir, purpose := range structure {
		fmt.Printf("%-12s %s\n", dir, purpose)
	}
}

Gotcha: Creating a services/, models/, controllers/ layout (MVC-style) — this groups by technical role rather than by business concern, scattering related code across directories.