Web Frameworks Basics
10 examples to get you started with Web Frameworks - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir webdemo && cd webdemo && go mod init example.com/webdemo. - Add dependencies as needed:
go get github.com/go-chi/chi/v5andgo get github.com/gin-gonic/gin. - Run examples with
go run .after saving asmain.go(or split files in the same module).
Basic Examples
1. Stdlib hello handler
The baseline every framework builds on: http.HandlerFunc.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello, stdlib")
})
http.ListenAndServe(":8080", nil)
}http.HandleFuncregisters onDefaultServeMux.- Handlers receive
ResponseWriterandRequest- the universal contract. - Prefer explicit
http.Serverin production; this shows the smallest shape.
Related: net/http: Go's Batteries-Included HTTP Stack - stdlib HTTP model
2. chi router with one GET route
chi stays compatible with http.Handler while adding path params.
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
http.ListenAndServe(":8080", r)
}chi.NewRouter()returns anhttp.Handleryou pass toListenAndServe.- Route methods (
Get,Post) set HTTP verb constraints. - No framework-specific context - handlers look like stdlib code.
Related: chi: Lightweight Routing & Middleware - route groups and middleware
3. chi URL parameter
Named segments appear in the pattern and are read from the request.
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
w.Write([]byte("user " + id))
}){id}binds one path segment; use{id:*}for greedy rest paths when needed.chi.URLParamreturns empty string if the key is missing - validate before use.- Param names are per-route; document them in OpenAPI or route tables.
Related: Go HTTP Frameworks: Routers vs Full Frameworks - router vs framework model
4. chi middleware chain
Use wraps all routes registered after it on that router.
import "github.com/go-chi/chi/v5/middleware"
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Get("/time", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})- Middleware order matters: first
Useis outermost on the way in. - chi ships common middleware: request ID, recoverer, timeout, compress.
- Write custom middleware as
func(http.Handler) http.Handler.
Related: Middleware & Decorator Patterns - handler wrapping model
5. chi route group with prefix
Mount related routes under a shared path and middleware.
r.Route("/api/v1", func(r chi.Router) {
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
r.Get("/version", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"version":"1"}`))
})
})Routecreates a sub-router inheriting parent middleware unless you branch earlier.- Prefixes stack:
/api/v1/healthmatches the health handler. - Groups keep OpenAPI tags and auth boundaries aligned with URL structure.
6. Gin default engine and JSON route
Gin uses *gin.Context instead of raw http.ResponseWriter.
package main
import "github.com/gin-gonic/gin"
func main() {
g := gin.Default() // Logger + Recovery middleware
g.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
g.Run(":8080")
}gin.Default()enables debug logging middleware - usegin.New()plus explicit middleware in production tuning.c.JSONsetsContent-Typeand encodes the map.gin.His amap[string]anyshorthand for small payloads.
Related: Gin: Fast JSON APIs & Binding - binding and validation
7. Gin path parameter
Framework context exposes params with Param.
g.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
})- Gin uses
:idsyntax; chi uses{id}- migration requires path rewrites or adapters. - Params are always strings; parse integers with
strconvor binding helpers. - Wildcard routes use
*pathwith slightly different semantics than chi's{path:*}.
Intermediate Examples
8. Gin JSON bind on POST
Bind request bodies into structs with tags.
type createUser struct {
Email string `json:"email" binding:"required,email"`
}
g.POST("/users", func(c *gin.Context) {
var in createUser
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(201, gin.H{"email": in.Email})
})ShouldBindJSONavoids double-writing responses on failure (prefer overBindJSONin handlers).- Validation tags need
github.com/go-playground/validatorwired through Gin. - Keep DTO structs in a
apiortransportpackage separate from domain models.
9. chi sub-router mounted at path
Mount independent routers for modular services.
admin := chi.NewRouter()
admin.Get("/stats", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("stats"))
})
r.Mount("/admin", admin)Mountstrips the prefix before the sub-router matches.- Each mounted router can carry its own middleware stack.
- Useful for splitting admin, public API, and webhooks in one binary.
10. Shared business handler behind two routers
Write logic once as http.HandlerFunc, register on chi; Gin needs a thin adapter.
func showHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true}`))
}
// chi
r.Get("/health", showHealth)
// gin adapter
g.GET("/health", func(c *gin.Context) {
showHealth(c.Writer, c.Request)
})- Portable handlers ease migration between routers.
- Gin's
c.Writerandc.Requestare the underlying stdlib types. - For larger apps, mount an entire
chi.Routerunder Gin or vice versa instead of per-route adapters.
Related: Migrating Between Routers Without Rewriting Handlers - adapter patterns
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).