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.go — main 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.