Day 9 — Packages & Modules
Learning Objectives
- Understand Go's flat package system and why circular imports are impossible
- Structure code for testability and clear ownership
- Know what
go.modandgo.sumguarantee about dependency integrity
Key Concepts
// Package naming rules
package main // executable
package config // matches directory name
package http // stdlib exception: matches "net/http" last segment
// Exported vs unexported
type Handler struct { } // exported — visible outside package
type handler struct { } // unexported — package-private
func Process() { } // exported
func validate() { } // unexported
// internal/ — only parent packages can import
myproject/
├── internal/
│ └── crypto/ // only myproject/* can import this
└── api/
└── handler.go // can import internal/crypto
Project Structure for Security Proxy
secproxy/
├── go.mod
├── main.go
├── config/
│ └── config.go // Config struct, Load(), Validate()
├── storage/
│ └── storage.go // Storage interface + MemoryStorage
├── server/
│ └── server.go // HTTP server setup, middleware chain
├── health/
│ └── health.go // /health endpoint handler
└── internal/
└── ratelimit/
└── ratelimit.go // not exported — implementation detail
Exercise: Refactor Week 1 into Packages
// config/config.go
package config
import (
"encoding/json"
"fmt"
"os"
"sync"
"time"
)
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
MaxConns int `json:"max_connections"`
WAFMode string `json:"waf_mode"`
RateLimitRPS int `json:"rate_limit_rps"`
}
type Store struct {
mu sync.RWMutex
cfg Config
startedAt time.Time
}
func Load(path string) (*Store, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("config.Load: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("config.Load: invalid JSON: %w", err)
}
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("config.Load: %w", err)
}
return &Store{cfg: cfg, startedAt: time.Now()}, nil
}
func (c *Config) validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("port %d out of range", c.Port)
}
if c.MaxConns < 1 {
return fmt.Errorf("max_connections must be >= 1")
}
switch c.WAFMode {
case "off", "detection", "blocking":
default:
return fmt.Errorf("waf_mode must be off|detection|blocking, got %q", c.WAFMode)
}
return nil
}
func (s *Store) Get() Config {
s.mu.RLock()
defer s.mu.RUnlock()
return s.cfg
}
func (s *Store) Reload(path string) error {
newStore, err := Load(path)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
s.cfg = newStore.cfg
return nil
}
func (s *Store) Uptime() time.Duration { return time.Since(s.startedAt) }
Real-World Context
The internal/ directory prevents external consumers of your library from depending on implementation details. config.Store with Reload() is exactly the pattern for hot-reloading WAF rules in Day 85 — SIGHUP calls Reload() and the new config is live within milliseconds.