Search and navigate

Pages

Start HereRoadmapTutorialsCheatsheetInterview QuestionsResources

Published tutorials

Building a REST API with Go: The Complete TutorialGeneric Functions in Go: Writing Reusable CodeGo Benchmarking: Measuring Performance with testing.BGo Channels: Communication Between GoroutinesGo Constraints: comparable, Ordered and Custom SetsGo Context: Cancellation, Deadlines, and Request ValuesGo HTTP Client: Timeouts, JSON, and Reliable RequestsGo HTTP Middleware: Build a Production ChainGo Interview Questions: 25 Answers That Show UnderstandingGo Mutex: Protecting Shared State from Race ConditionsGo net/http: Build an HTTP Server from ScratchGo select Statement: Multiplexing Channels ExplainedGo Tools You Need for a Reliable Development LoopGo Tutorial: From First Program to Backend FoundationsGo Type Parameters: Syntax and Constraints ExplainedGo Worker Pool Pattern: Bounded Concurrency at ScaleGo's Type System Deep Dive: Static Typing Done RightGolang Compilation and Execution ExplainedGolang File Structure for Modules and ApplicationsGolang Hello World and Your First Go ProgramGolang IDEs: Choose an Editor for Your WorkflowGolang Interfaces: What They Are and How to Use ThemGoroutine Leaks in Go: How to Detect and Prevent ThemGoroutines in Go: Concurrency Without LeaksHow to Download and Install Golang SafelyHow to Learn Golang with a Focused Study PlanIntegration Testing in Go: Testing Real BoundariesJSON in Go: Encoding, Decoding, and ValidationLearn Golang with a Practical Beginner CourseMocking in Go: Test Doubles Without Magicsync.WaitGroup in Go: Coordinating Multiple GoroutinesTable-Driven Tests in Go: The Idiomatic WayTesting in Go: Table Tests, HTTP, Fakes, and CoverageWhat Is Go and How Does a Go Program Work?Why Learn Golang and What Is Go Used For?Writing Unit Tests in Go: A Practical Guide
Open full search and filters →

Go HTTP Middleware: Build a Production Chain

Build composable Go HTTP middleware for logging, recovery, request IDs, authentication, and timeouts.

Standard-library lessonGo requirement: Go 1.22+How tutorials are checked
Web & APIs · Lesson 4Saved in this browser. No account required.

Verification (10 August 2026): Self-contained runnable examples were compile-tested with Go 1.26.5 on Windows/amd64. Multi-file or contextual snippets and intentional compiler-error demonstrations were checked separately in the repository verification matrix.

HTTP middleware wraps a handler to run logic before or after the request. The standard shape is a function from http.Handler to http.Handler.

Write one middleware

type Middleware func(http.Handler) http.Handler

func requestID(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := cryptoRandID()
		w.Header().Set("X-Request-ID", id)
		ctx := context.WithValue(r.Context(), requestIDKey{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

Middleware should pass the original request or a deliberate copy to next. If it decides the request is invalid, it writes a response and returns without calling the next handler.

Compose a chain

func chain(handler http.Handler, middleware ...Middleware) http.Handler {
	for i := len(middleware) - 1; i >= 0; i-- {
		handler = middleware[i](handler)
	}
	return handler
}

handler := chain(mux, recoverPanic, requestID, accessLog)

The first middleware listed is the outermost wrapper. Ordering matters: recovery should normally surround anything that might panic; request IDs should exist before logging needs them; authentication should run before protected handlers.

The request enters the outermost middleware first; deferred and response-side work unwinds from the handler back toward the outer wrapper.
Middleware execution order showing a request entering recovery, request ID, and access logging before the handler and the response returning in reverse order

Recover without hiding failures

func recoverPanic(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if value := recover(); value != nil {
				log.Printf("panic: %v\n%s", value, debug.Stack())
				http.Error(w, "internal server error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

Recovery keeps one request from terminating the process, but it must record enough information to debug the defect.

Response-writer wrappers need care

Logging status codes requires a wrapper around http.ResponseWriter. Preserve optional interfaces such as http.Flusher when streaming or WebSockets matter. A simplistic wrapper can silently break handlers.

Keep middleware focused, dependency-injected, and independently testable with httptest. Business rules belong in services or handlers, not a growing global chain.