Lead Go Architect
name: go-architect
by anderskev · published 2026-04-01
$ claw add gh:anderskev/anderskev-go-architect---
name: go-architect
description: Go application architecture with net/http 1.22+ routing, project structure patterns, graceful shutdown, and dependency injection. Use when building Go web servers, designing project layout, or structuring application dependencies.
---
# Lead Go Architect
Quick Reference
| Topic | Reference |
|-------|-----------|
| Flat vs modular project layout, migration signals | [references/project-structure.md](references/project-structure.md) |
| Graceful shutdown with signal handling | [references/graceful-shutdown.md](references/graceful-shutdown.md) |
| Dependency injection patterns, testing seams | [references/dependency-injection.md](references/dependency-injection.md) |
Core Principles
1. **Standard library first** -- Use `net/http` and the Go 1.22+ enhanced `ServeMux` for routing. Only reach for a framework (chi, echo, gin) when you have a concrete need the stdlib cannot satisfy (e.g., complex middleware chains, regex routes).
2. **Dependency injection over globals** -- Pass databases, loggers, and services through struct fields and constructors, never package-level `var`.
3. **Explicit over magic** -- No `init()` side effects, no framework auto-wiring. `main.go` is the composition root where everything is assembled visibly.
4. **Small interfaces, big structs** -- Define interfaces at the consumer, keep them narrow (1-3 methods). Concrete types carry the implementation.
Go 1.22+ Enhanced Routing
Go 1.22 upgraded `http.ServeMux` with method-based routing and path parameters, eliminating the most common reason for third-party routers.
Method-Based Routing and Path Parameters
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", s.handleListUsers)
mux.HandleFunc("GET /api/users/{id}", s.handleGetUser)
mux.HandleFunc("POST /api/users", s.handleCreateUser)
mux.HandleFunc("DELETE /api/users/{id}", s.handleDeleteUser)Extracting Path Parameters
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
user, err := s.users.GetUser(r.Context(), id)
if err != nil {
s.logger.Error("getting user", "err", err, "id", id)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}Wildcard and Exact Match
// Exact match on trailing slash -- serves /api/files/ only
mux.HandleFunc("GET /api/files/", s.handleListFiles)
// Wildcard to end of path -- /api/files/path/to/doc.txt
mux.HandleFunc("GET /api/files/{path...}", s.handleGetFile)Routing Precedence
The new `ServeMux` uses most-specific-wins precedence:
Server Struct Pattern
The Server struct is the central dependency container for your application. It holds all shared dependencies and implements `http.Handler`.
type Server struct {
db *sql.DB
logger *slog.Logger
router *http.ServeMux
}
func NewServer(db *sql.DB, logger *slog.Logger) *Server {
s := &Server{
db: db,
logger: logger,
router: http.NewServeMux(),
}
s.routes()
return s
}
func (s *Server) routes() {
s.router.HandleFunc("GET /api/users/{id}", s.handleGetUser)
s.router.HandleFunc("POST /api/users", s.handleCreateUser)
s.router.HandleFunc("GET /healthz", s.handleHealth)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}Middleware Wrapping
Apply middleware at the `http.Server` level or per-route:
// Wrap entire server
httpServer := &http.Server{
Addr: ":8080",
Handler: requestLogger(s),
}
// Or per-route
s.router.Handle("GET /api/admin/", adminOnly(http.HandlerFunc(s.handleAdmin)))Middleware Signature
func requestLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
})
}Project Structure
Choose based on project size:
Start flat. Migrate when you see the signs described in the reference.
Graceful Shutdown
Every production Go server needs graceful shutdown. The pattern uses `signal.NotifyContext` to listen for OS signals and `http.Server.Shutdown` to drain connections.
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer cancel()
// ... start server in goroutine ...
<-ctx.Done()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
httpServer.Shutdown(shutdownCtx)Full pattern with cleanup ordering in [references/graceful-shutdown.md](references/graceful-shutdown.md).
When to Load References
Load **project-structure.md** when:
Load **graceful-shutdown.md** when:
Load **dependency-injection.md** when:
Anti-Patterns
Global database variables
// BAD -- untestable, hidden dependency
var db *sql.DB
func handleGetUser(w http.ResponseWriter, r *http.Request) {
db.QueryRow(...)
}Pass `db` through a Server or Service struct instead.
Framework-first thinking
Do not start with `gin.Default()` or `echo.New()`. Start with `http.NewServeMux()`. Only introduce a framework if you hit a real limitation of the stdlib that justifies the dependency.
God packages
A single `handlers` package with 50 files is not organization. Group by domain (`user`, `order`, `billing`), not by technical layer.
Using init() for setup
// BAD -- invisible side effects, untestable
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}All initialization belongs in `main()` or a `run()` function so it can be tested and errors can be handled.
Reading config in business logic
// BAD -- couples handler to environment
func (s *Server) handleSendEmail(w http.ResponseWriter, r *http.Request) {
apiKey := os.Getenv("SENDGRID_API_KEY") // don't do this
}Inject configuration values or clients through constructors.
More tools from the same signal band
Order food/drinks (点餐) on an Android device paired as an OpenClaw node. Uses in-app menu and cart; add goods, view cart, submit order (demo, no real payment).
Sign plugins, rotate agent credentials without losing identity, and publicly attest to plugin behavior with verifiable claims and authenticated transfers.
The philosophical layer for AI agents. Maps behavior to Spinoza's 48 affects, calculates persistence scores, and generates geometric self-reports. Give your...