Frontend Labs
⋯

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.mod and go.sum guarantee 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.


Generated from week2.md · 8 Oct 2026 16:36