go / handlers
I structure Go web handlers in the www/ package using flat composition
rather than heavy constructor patterns. Handlers receive dependencies from
the composition root and render HTML templates by converting typed query
rows into pre-formatted maps.
Handler Structure
Each feature handler embeds webdeps.Standard, which bundles common
runtime necessities (database pool, session helpers, render function):
package admincompanies
import "eds/www/webdeps"
type Handler struct {
webdeps.Standard
}
Feature handlers are constructed as simple struct literals in the
composition root (cmd/www/main.go):
adminCompaniesHandler := &admincompanies.Handler{Standard: std}
PageData to Map Rendering
While the Ruby counterpart enforces a template contract with Data.define
structs, Go handlers pass data to templates via map[string]any.
At the boundary to the template engine, maps are required because the rendering signatures are generic. However, the handler remains type-safe because it fetches and maps data using database-backed Go structs before formatting them:
type dbRow struct {
ID int64 `db:"id"`
CompanyName string `db:"company_name"`
EdsScore float64 `db:"eds_score"`
LastMetOn pgtype.Date `db:"last_met_on"`
}
func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
// 1. Validate params first
if err := webutil.ValidateParams(r, "query"); err != nil {
h.WriteError(w, 400, err.Error())
return
}
// 2. Fetch typed rows
rows, err := h.fetchRows(r.Context())
if err != nil {
h.WriteError(w, 500, "database error")
return
}
// 3. Map and format to view dictionary
viewRows := make([]map[string]any, 0, len(rows))
for _, row := range rows {
viewRows = append(viewRows, map[string]any{
"id": row.ID,
"company_name": row.CompanyName,
"eds_score": fmt.Sprintf("%.1f", row.EdsScore),
"last_met_on": formatDate(row.LastMetOn),
})
}
// 4. Render page
html, err := h.RenderPage(r, "admin/companies/index", map[string]any{
"rows": viewRows,
})
if err != nil {
h.WriteError(w, 500, err.Error())
return
}
h.WriteHTML(w, 200, html)
}
Why this pattern
- Explicit Params Validation: Every request must start with
webutil.ValidateParams. - Dumb Templates: All formatting (such as date parsing and floats) happens in Go where it is typechecked, ensuring templates remain logicless.
- Linear Debugging: Compile errors point immediately to the Go handler. There are no runtime dynamic type-casting issues.