Introduction to Web Development in Go
Introduction to Web Development in Go
Overview
Go is a strong fit for web backends: one binary, a solid standard library HTTP stack, clear concurrency, and boring reliability. This chapter sets the mental model—how a request moves through your process—and gets a minimal server running.
Why Go for web services
| Strength | What it means in practice |
|---|---|
net/http in stdlib |
Ship APIs without a framework tax |
| Goroutines | Handle many concurrent connections simply |
Static typing + go test |
Refactor routes and handlers with confidence |
| Single binary | Deploy the same artifact to VM, container, or bare metal |
| Explicit errors | Surface failures at the boundary instead of hidden exceptions |
You still need good structure, timeouts, and tests. Go does not replace design; it keeps the implementation honest.
Request lifecycle
Client Server process
│ │
│ TCP + HTTP request │
│───────────────────────►│ ListenAndServe accepts connection
│ │ ServeMux matches method + path
│ │ Middleware (log, auth, recover)
│ │ Handler reads body / query
│ │ Domain / DB / external calls
│ status + headers+body │
│◄───────────────────────│ Response written, connection reuse
Rule: Handlers should be thin. Parse input → call application code → write response. Keep SQL, payment calls, and business rules out of the raw http.Handler body when the app grows.
First server
mkdir -p /tmp/bookstore-intro && cd /tmp/bookstore-intro
go mod init example.com/bookstoreSave as main.go:
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", healthz)
mux.HandleFunc("GET /api/hello", hello)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Println("listening on :8080")
log.Fatal(srv.ListenAndServe())
}
func healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"service": "bookstore",
"message": "welcome",
})
}Run and probe:
go run .
# other terminal
curl -s localhost:8080/healthz
curl -s localhost:8080/api/helloWhat to notice
- Method-aware patterns (
GET /path) need Go 1.22+. Older Go used path-only matching. - Timeouts on
http.Serverare not optional for production; they bound stuck clients. - JSON via
encoding/jsonis enough for most REST bodies.
Status codes you will use constantly
| Code | When |
|---|---|
| 200 | Success with body |
| 201 | Created resource |
| 204 | Success, no body |
| 400 | Client sent bad input |
| 401 | Not authenticated |
| 403 | Authenticated but not allowed |
| 404 | Missing resource |
| 409 | Conflict (duplicate ISBN, etc.) |
| 500 | Unexpected server failure |
Prefer specific 4xx over a generic 500 when the client can fix the request.
Logging from day one
import "log/slog"
func withRequestLog(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,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}Wire it as Handler: withRequestLog(mux) on the server. Chapter 279 expands correlation and tests.
Error shape for APIs
Agree early on a small JSON error envelope:
type APIError struct {
Error string `json:"error"`
Code string `json:"code,omitempty"`
}
func writeError(w http.ResponseWriter, status int, code, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(APIError{Error: msg, Code: code})
}Clients and frontends can branch on code without scraping free-text messages.
Bookstore north star
By the end of this part the same process will expose:
GET /api/books— list catalogGET /api/books/{id}— book detailPOST /api/books— create (admin)POST /api/auth/login— session cookie- HTML pages that reuse the same domain layer
Rules of thumb
| Do | Don’t |
|---|---|
| Set server timeouts | Use bare http.ListenAndServe in production forever |
| Return structured JSON errors | Mix ad-hoc string bodies |
| Keep handlers thin | Put SQL and payment logic only in handlers |
| Log method, path, duration | Log secrets, cookies, full credit cards |
Try next
- Add
GET /api/versionreturning a build string. - Return 404 JSON for unknown paths via a catch-all or custom
NotFoundHandler. - Hit the server with concurrent
curlloops and watch logs stay readable.