Go HTTP Server: Building Web Servers from Scratch
Go’s standard library includes everything you need to build production web servers — no frameworks required. The net/http package provides an HTTP client and server implementation that’s fast, well-tested, and idiomatic.
The Basics: Hello World Server
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
---That’s it. Run go run main.go and visit http://localhost:8080. The server handles connections, parses requests, and sends responses using the standard library.
Handlers and Handler Functions
In Go, an HTTP handler implements the http.Handler interface:
type Handler interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
---http.HandlerFunc is a function type that implements Handler by calling itself:
type HandlerFunc func(w http.ResponseWriter, r *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
---This means any function with the signature func(w http.ResponseWriter, r *http.Request) can be used as a handler.
Route Matching
Go’s default ServeMux (multiplexer) matches the longest prefix:
http.HandleFunc("/", homePage) // catches everything
http.HandleFunc("/api", apiHome) // catches /api, /api/anything
http.HandleFunc("/api/users", users) // catches /api/users, /api/users/123Patterns ending with / are subtree patterns — they match any path starting with that prefix. Patterns without a trailing / are exact matches.
// Exact match — only "/about"
http.HandleFunc("/about", aboutPage)
// Subtree match — "/api/" and anything under it
http.HandleFunc("/api/", apiHandler)Go 1.22 introduced enhanced routing with method matching and path parameters:
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// Access path parameters
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "User ID: %s", id)
---The Request and Response
Reading the Request
func handler(w http.ResponseWriter, r *http.Request) {
// Method
fmt.Println(r.Method)
// URL components
fmt.Println(r.URL.Path)
fmt.Println(r.URL.Query().Get("page"))
// Headers
fmt.Println(r.Header.Get("Content-Type"))
// Body
body, _ := io.ReadAll(r.Body)
defer r.Body.Close()
fmt.Println(string(body))
// Form data
r.ParseForm()
fmt.Println(r.FormValue("username"))
// JSON
var data map[string]interface{}
json.NewDecoder(r.Body).Decode(&data)
---Writing the Response
func jsonResponse(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := map[string]string{"status": "ok", "message": "Hello, JSON!"}
json.NewEncoder(w).Encode(response)
---
func fileResponse(w http.ResponseWriter, r *http.Request) {
// Serve a file
http.ServeFile(w, r, "static/index.html")
// Or serve from a file system
fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
---Custom ServeMux and Routing
For complex routing, build a custom multiplexer:
type Router struct {
routes []Route
---
type Route struct {
Method string
Pattern string
Handler http.HandlerFunc
---
func (r *Router) Handle(method, pattern string, handler http.HandlerFunc) {
r.routes = append(r.routes, Route{Method: method, Pattern: pattern, Handler: handler})
---
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for _, route := range r.routes {
if route.Method != "" && req.Method != route.Method {
continue
}
if match, _ := path.Match(route.Pattern, req.URL.Path); match {
route.Handler(w, req)
return
}
}
http.NotFound(w, req)
---Most projects use a third-party router like chi or gorilla/mux for advanced features, but the standard library is sufficient for many APIs.
Middleware
Middleware wraps a handler to add cross-cutting behavior — logging, authentication, rate limiting, recovery.
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
---
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !isValidToken(token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
---
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
---
// Chain middleware
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
handler := Recovery(Logging(Auth(mux)))
http.ListenAndServe(":8080", handler)
---Middleware With Configuration
func RateLimit(requests int, per time.Duration) func(http.Handler) http.Handler {
limiter := rate.NewLimiter(rate.Limit(requests)/per.Seconds(), requests)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
---Static File Serving
Go can serve static files with a single line. For production, use the embed package to bundle files into the binary:
import "embed"
//go:embed static/*
var staticFiles embed.FS
func main() {
fs := http.FileServer(http.FS(staticFiles))
http.Handle("/static/", http.StripPrefix("/static/", fs))
---For deployment, embedded files eliminate the need to manage separate static file directories. The binary contains everything needed to run.
HTTPS and TLS
Enable TLS by providing a certificate and key:
srv := &http.Server{
Addr: ":443",
Handler: router,
---
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))For production, use Let’s Encrypt with golang.org/x/crypto/acme/autocert for automatic certificate renewal:
m := &autocert.Manager{
Cache: autocert.DirCache("certs"),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.com"),
---
srv := &http.Server{
Addr: ":https",
TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
---Structured Logging
Add request logging with duration and status code tracking:
type responseWriter struct {
http.ResponseWriter
status int
---
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
---
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rw, r)
log.Printf("method=%s path=%s status=%d duration=%s",
r.Method, r.URL.Path, rw.status, time.Since(start))
})
---Structured logging (key=value format) makes it easy to search logs and integrate with log aggregation tools.
Graceful Shutdown
Production servers must shut down gracefully — finish in-flight requests, close connections, clean up resources:
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("server forced to shutdown: %v", err)
}
---Rate Limiting
Protect your server from abuse with a simple token bucket rate limiter:
func rateLimit(next http.Handler, requestsPerSec int) http.Handler {
limiter := rate.NewLimiter(rate.Limit(requestsPerSec), 1)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "429 Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
---Combine rate limiting with IP-based tracking for per-client limits. Store token buckets per IP in a sync.Map or Redis for distributed deployments.
Testing HTTP Handlers
Go’s httptest package provides utilities for testing handlers without a running server:
func TestGreeting(t *testing.T) {
req := httptest.NewRequest("GET", "/greet?name=Alice", nil)
rec := httptest.NewRecorder()
greetHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rec.Code)
}
expected := `{"message":"Hello, Alice"}`
if rec.Body.String() != expected {
t.Errorf("expected %q, got %q", expected, rec.Body.String())
}
---Production Configuration
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20, // 1 MB
---Summary
Go’s standard library provides a complete HTTP server framework. Handlers and middleware compose cleanly through the http.Handler interface. With graceful shutdown, proper timeouts, and middleware chains, you can build production-ready web services without external dependencies.
Related: Go REST API Guide | Go Web Development
Advanced Patterns in Go
Context Propagation
Go’s context package is essential for managing timeouts, cancellations, and request-scoped values across API boundaries and goroutines:
func handler(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result, err := queryDatabase(ctx)
if err != nil {
return fmt.Errorf("query failed: %w", err)
}
return result
---Memory Management
Go’s garbage collector has been optimized significantly since version 1.5. The GC is concurrent and generational, with typical pause times under 1ms. For latency-critical applications, you can tune GC behavior with GOGC (target heap growth percentage) and GOMEMLIMIT (soft memory cap).
# Aggressive GC for latency-sensitive apps
export GOGC=50 # trigger GC when heap grows 50%
export GOMEMLIMIT=2GiB # soft memory limitBuild Tags and Conditional Compilation
Build tags let you compile platform-specific or feature-flag code:
//go:build linux
// +build linux
package main
func getOS() string {
return "linux"
---go build -tags=debug,linux .Pprof Profiling
import _ "net/http/pprof"
// Access profiles at /debug/pprof/
go tool pprof http://localhost:8080/debug/pprof/heap
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30FAQ
Q: Do I need a third-party router for Go HTTP servers?
A: No. Go 1.22+ ServeMux supports method-based routing and path parameters. Third-party routers like chi or gorilla/mux offer additional features but are not required.
Q: How do I set response headers in Go?
A: Use w.Header().Set("Key", "Value") before calling w.WriteHeader(). Headers must be set before the first write.
Q: What timeouts should I set on a production server?
A: Set ReadTimeout (10s), WriteTimeout (10s), IdleTimeout (120s), and MaxHeaderBytes (1MB). These prevent slow clients from consuming server resources.
Q: How do I handle CORS in Go?
A: Set CORS headers (Access-Control-Allow-Origin, etc.) in a middleware handler. For preflight OPTIONS requests, return 204 with the appropriate headers.
Q: Can I serve HTTP and HTTPS on the same port? A: No. Use separate ports (e.g., 80 for HTTP redirect to HTTPS, 443 for HTTPS) or use a reverse proxy like Nginx or Caddy.
Related Concepts and Further Reading
Understanding go http server requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.
The relationship between go http server and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.
For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of go http server. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.