Loading...
Loading...
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.
npx skill4agent add existential-birds/beagle 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/httpServeMuxvarinit()main.gohttp.ServeMuxmux := 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)ServeMuxGET /api/users/{id}GET /api/users/GET /api/users/meGET /api/users/{id}http.Handlertype 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)
}http.Server// 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))
})
}cmd/internal/signal.NotifyContexthttp.Server.Shutdownctx, 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)// BAD -- untestable, hidden dependency
var db *sql.DB
func handleGetUser(w http.ResponseWriter, r *http.Request) {
db.QueryRow(...)
}dbgin.Default()echo.New()http.NewServeMux()handlersuserorderbilling// BAD -- invisible side effects, untestable
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}main()run()// 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
}