Install
openclaw skills install go-architectGo 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.
openclaw skills install go-architect| Topic | Reference |
|---|---|
| Flat vs modular project layout, migration signals | references/project-structure.md |
| Graceful shutdown with signal handling | references/graceful-shutdown.md |
| Dependency injection patterns, testing seams | references/dependency-injection.md |
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).var.init() side effects, no framework auto-wiring. main.go is the composition root where everything is assembled visibly.Use this sequence when implementing or reviewing work that claims to follow this skill. Do not skip ahead; each step has a pass condition you can answer with tooling or a concrete file path.
ServeMux features (method+path patterns like "GET /x/{id}", r.PathValue, or {path...}): run go version and pass only if the reported toolchain is go1.22+. If the project must stay on an older Go, pass only by not using those APIs (use a compatible router or older patterns) and say so in the review or PR.main.go or cmd/.../main.go visibly constructs the server and injects shared dependencies (DB, logger, config). Fail if shared dependencies are wired in init() or package-level var instead of explicit construction in main (or a run() called from main).http.Server.Shutdown with a bounded context (e.g. context.WithTimeout) after waiting on signal.NotifyContext (or equivalent). Cite the file path when reporting; see references/graceful-shutdown.md for the full pattern.os.Getenv for secrets or use package-level var for DB/clients (loading env in main or a dedicated config package is fine).Go 1.22 upgraded http.ServeMux with method-based routing and path parameters, eliminating the most common reason for third-party routers.
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)
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)
}
// 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)
The new ServeMux uses most-specific-wins precedence:
GET /api/users/{id} is more specific than GET /api/users/GET /api/users/me is more specific than GET /api/users/{id}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)
}
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)))
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))
})
}
Choose based on project size:
cmd/, internal/ with domain packages. For larger apps with multiple bounded contexts. See references/project-structure.md.Start flat. Migrate when you see the signs described in the reference.
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.
Load project-structure.md when:
Load graceful-shutdown.md when:
Load dependency-injection.md when:
// 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.
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.
A single handlers package with 50 files is not organization. Group by domain (user, order, billing), not by technical layer.
// 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.
// 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.