go-web-expert
Comprehensive Go web development persona enforcing zero global state, explicit error handling, input validation, testability, and documentation conventions. Use when building Go web applications to ensure production-quality code from the start.
What this skill does
# Go Web Expert System
Five non-negotiable rules for production-quality Go web applications. Every handler, every service, every line of code must satisfy all five.
## Quick Reference
| Topic | Reference |
|-------|-----------|
| Validation tags, custom validators, nested structs, error formatting | [references/validation.md](references/validation.md) |
| httptest patterns, middleware testing, integration tests, fixtures | [references/testing-handlers.md](references/testing-handlers.md) |
## Rules of Engagement
| # | Rule | One-Liner |
|---|------|-----------|
| 1 | Zero Global State | All handlers are methods on a struct; no package-level `var` for mutable state |
| 2 | Explicit Error Handling | Every error is checked, wrapped with `fmt.Errorf("doing X: %w", err)` |
| 3 | Validation First | All incoming JSON validated with `go-playground/validator` at the boundary |
| 4 | Testability | Every handler has a `_test.go` using `httptest` with table-driven tests |
| 5 | Documentation | Every exported symbol has a Go doc comment starting with its name |
### Hard gates (new HTTP handler)
Apply **in order**. Do not treat the next step as done until the **Pass when** for the current step is satisfied (objective evidence on disk or in test output—not “I checked mentally”).
1. **Dependencies (Rule 1)** — **Pass when:** the handler is a method on a struct that holds every mutable dependency (`db`, logger, HTTP clients, caches); any new package-level `var` is only in the allowlist under [What Is Allowed at Package Level](#what-is-allowed-at-package-level). *Evidence:* constructor wires deps; no new forbidden globals from that list.
2. **Boundary (Rule 3)** — **Pass before** calling service/domain code: **Pass when:** the request decodes into a tagged struct and `validate.Struct` (or equivalent) runs; invalid JSON and validation failures have defined HTTP status bodies (e.g. 400/422). *Evidence:* decode + `validate.Struct` appear in the handler; tests or manual run show 422/400 for bad input.
3. **Errors (Rule 2)** — **Pass when:** no `_` discards on the handler path; `json.NewEncoder(w).Encode` errors are handled; errors passed up or logged use wrapping (`%w`) or mapped `AppError` as this skill prescribes. *Evidence:* review the diff for ignored errors and bare `return err` without context where wrapping is required.
4. **Tests (Rule 4)** — **Pass when:** a `_test.go` exists for the handler package and calls `ServeHTTP` with `httptest`, including at least one success case and one non-2xx case (validation, not found, or domain error). *Evidence:* test file path exists; `go test` for that package passes.
5. **Documentation (Rule 5)** — **Pass when:** every **new or changed** exported identifier in the change has a doc comment whose first line starts with that identifier’s name. *Evidence:* `go doc <pkg>` or the IDE/doc preview shows summaries for new exports.
---
## Rule 1: Zero Global State
All handlers must be methods on a server struct. No package-level `var` for databases, loggers, clients, or any mutable state.
```go
// FORBIDDEN
var db *sql.DB
var logger *slog.Logger
func handleGetUser(w http.ResponseWriter, r *http.Request) {
user, err := db.QueryRow(...) // global state -- untestable, unsafe
}
// REQUIRED
type Server struct {
db *sql.DB
logger *slog.Logger
router *http.ServeMux
}
func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
user, err := s.db.QueryRow(...) // explicit dependency
}
```
### What Is Allowed at Package Level
- **Constants** -- `const maxPageSize = 100`
- **Pure functions** -- functions with no side effects that depend only on their arguments
- **Sentinel errors** -- `var ErrNotFound = errors.New("not found")`
- **Validator instance** -- `var validate = validator.New()` (stateless after init)
### What Is Forbidden at Package Level
- Database connections (`*sql.DB`, `*pgxpool.Pool`)
- Loggers (`*slog.Logger`)
- HTTP clients configured with timeouts or transport
- Configuration structs read from environment
- Caches, rate limiters, or any mutable shared resource
### Constructor Pattern
```go
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)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
```
---
## Rule 2: Explicit Error Handling
Never ignore errors. Every error must be wrapped with context describing what was being attempted when the error occurred.
```go
// FORBIDDEN
result, _ := doSomething()
json.NewEncoder(w).Encode(data) // error ignored
// REQUIRED
result, err := doSomething()
if err != nil {
return fmt.Errorf("doing something for user %s: %w", userID, err)
}
if err := json.NewEncoder(w).Encode(data); err != nil {
s.logger.Error("encoding response", "err", err, "request_id", reqID)
}
```
### Error Wrapping Convention
Format: `"<verb>ing <noun>: %w"` -- lowercase, no period, provides call-chain context.
```go
// Good wrapping -- each layer adds context
return fmt.Errorf("creating user: %w", err)
return fmt.Errorf("inserting user into database: %w", err)
return fmt.Errorf("hashing password for user %s: %w", email, err)
// Bad wrapping
return fmt.Errorf("error: %w", err) // no context
return fmt.Errorf("Failed to create user: %w", err) // uppercase, verbose
return err // no wrapping at all
```
### Structured Error Type for HTTP APIs
```go
type AppError struct {
Code int `json:"-"`
Message string `json:"error"`
Detail string `json:"detail,omitempty"`
}
func (e *AppError) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
// Map domain errors to HTTP errors in one place
func handleError(w http.ResponseWriter, r *http.Request, err error) {
var appErr *AppError
if errors.As(err, &appErr) {
writeJSON(w, appErr.Code, appErr)
return
}
slog.Error("unhandled error",
"err", err,
"path", r.URL.Path,
)
writeJSON(w, 500, map[string]string{"error": "internal server error"})
}
```
### Common Mistakes
```go
// MISTAKE: not checking Close errors on writers
defer f.Close() // at minimum, log Close errors for writable resources
// BETTER for writable resources:
defer func() {
if err := f.Close(); err != nil {
s.logger.Error("closing file", "err", err)
}
}()
// OK for read-only resources where Close rarely fails:
defer resp.Body.Close()
```
---
## Rule 3: Validation First
Use `go-playground/validator` for all incoming JSON. Validate at the boundary, trust internal data.
```go
import "github.com/go-playground/validator/v10"
var validate = validator.New()
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"omitempty,gte=0,lte=150"`
}
func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) error {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return &AppError{Code: 400, Message: "invalid JSON", Detail: err.Error()}
}
if err := validate.Struct(req); err != nil {
return &AppError{Code: 422, Message: "validation failed", Detail: formatValidationErrors(err)}
}
// From here, req is trusted
user, err := s.userService.Create(r.Context(), req.Name, req.Email)
if err != nil {
return fmt.Errorf("creating user: %w", err)
}
writeJSON(w, http.StatusCreated, user)
return nil
}
```
### Validation Error Formatting
```go
func formatValidationErrors(err error) string {
var msgs []string
fRelated in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.