AlgoMaster Logo

HTTP Handlers & HandlerFunc

High Priority19 min readUpdated June 6, 2026

A handler is the piece of code that turns an incoming HTTP request into a response. Go's net/http package keeps that contract small: any type with a ServeHTTP(w, r) method counts as a handler. This chapter covers the http.Handler interface, the http.HandlerFunc adapter that lets a plain function satisfy that interface, the rules for writing safely to an http.ResponseWriter, and the helpers (http.Error, http.NotFound, http.Redirect) that handle the common cases for you.

The http.Handler Interface

http.Handler is one of the smallest interfaces in the standard library:

Any type with that one method is a handler. The HTTP server hands each incoming request to a handler's ServeHTTP, which is expected to write a response back through w before returning. When ServeHTTP returns, the server treats the response as complete.

The cleanest reason to implement http.Handler on a struct rather than using a plain function is when the handler needs dependencies. A product lookup endpoint needs access to a product store; a checkout endpoint needs a payment processor. Putting those dependencies on a struct is the idiomatic way to wire them in.

A curl http://localhost:8080/product against this server prints:

The handler holds the store as a field, so ServeHTTP can reach the data it needs without globals. The *ProductHandler (pointer receiver) is registered with http.Handle, which expects an http.Handler. Since *ProductHandler has a ServeHTTP method, it satisfies the interface implicitly. There's no implements Handler keyword in Go.

http.HandlerFunc: The Function Adapter

Writing a struct for every handler gets noisy when the handler doesn't need any state. For these cases, Go uses an adapter type:

http.HandlerFunc is a function type with a ServeHTTP method defined on it. The method just calls the function. The effect is that any function with the right signature can be converted to an http.HandlerFunc, which then satisfies http.Handler because it has the required method.

The conversion http.HandlerFunc(listCategories) turns the function into a value of type http.HandlerFunc, which is a Handler. Calling curl http://localhost:8080/categories prints:

This pattern is so common that the standard library provides http.HandleFunc (no inner type conversion needed at the call site):

The two are equivalent. http.HandleFunc does the http.HandlerFunc(...) wrap for you internally. Use whichever reads more clearly. Most code uses HandleFunc for free functions and Handle for struct handlers.

HandlerFunc exists to let plain functions and method-bearing types share the same interface. Without it, you'd need a struct for every handler, even one-liners.

ResponseWriter Rules

http.ResponseWriter is the interface a handler writes the response through. It has three methods:

The order in which you call these matters. The HTTP response on the wire is: status line, then headers, then a blank line, then body. The ResponseWriter mirrors that order strictly.

MethodWhat It DoesWhen to Call
Header()Returns the header map you mutate.Before WriteHeader or first Write.
WriteHeader(code)Sends the status line and all headers. Once only.After all headers are set, before Write.
Write(p)Writes body bytes. First call sends status 200 if WriteHeader wasn't called.After WriteHeader (or as the first send).

The three rules:

  1. Headers must be set before `WriteHeader`. Once WriteHeader runs, headers are flushed to the response. Later Header().Set calls modify the map but no client will ever see them.
  2. `WriteHeader` must run before `Write`. If you call Write first, the server implicitly sends a 200 OK and flushes headers for you. After that, calling WriteHeader is a no-op (and triggers a warning to the server log).
  3. `WriteHeader` may be called only once. A second call is ignored, but net/http logs a warning so the bug is visible during development.

A short handler that follows the rules:

The Content-Type and Cache-Control headers were set before WriteHeader, so they made it onto the wire. The body was written after, which is how the rule expects you to do it.

Status Codes

The net/http package defines named constants for every HTTP status code. Using them instead of raw integers makes the intent obvious and prevents typos.

ConstantCodeMeaning
http.StatusOK200Success.
http.StatusCreated201New resource created.
http.StatusNoContent204Success, no body.
http.StatusBadRequest400Client sent something invalid.
http.StatusNotFound404Resource doesn't exist.
http.StatusMethodNotAllowed405Wrong method for this path.
http.StatusInternalServerError500Server-side failure.

A handler that branches on the request and picks the right status:

Each branch returns early after writing its response. That pattern, write-then-return, is what keeps the once-only WriteHeader rule safe. If you forget the return, execution falls through to the success branch and tries to call WriteHeader again.

The http.Error Helper

Writing Header().Set + WriteHeader(code) + Fprintln every time you need to send an error gets repetitive. http.Error is a one-line shortcut:

It sets Content-Type: text/plain; charset=utf-8, sets X-Content-Type-Options: nosniff (a small security header that tells browsers not to guess the type), writes the status code, and writes the error message followed by a newline.

The same getProduct handler with http.Error:

Less code, same behavior. The crucial detail is the return after each http.Error. The helper does not stop execution; it writes the response and comes back. If you omit the return, the handler keeps running and may write more bytes, which corrupts the response.

What's wrong with this code?

The handler calls http.Error for a wrong method but doesn't return. Execution falls through to fmt.Fprintln, which appends order deleted to the body the helper already wrote. The client sees a 405 status with body:

Fix:

Forgetting return after http.Error is one of the most common bugs in handler code. Every error branch needs an explicit return.

NotFound and Redirect Helpers

Two more helpers cover the most common one-liner responses.

http.NotFound(w, r) is http.Error(w, "404 page not found", http.StatusNotFound) with the standard wording. It exists so 404 responses across a codebase look consistent.

http.Redirect(w, r, url, code) writes a redirect response. The code is the redirect status; the most common choices are http.StatusFound (302, temporary) and http.StatusMovedPermanently (301, permanent). The helper sets the Location header to url, writes the status, and writes a small HTML body.

A curl -L (which follows redirects) lands on /cart and prints Your cart has 3 items. instead. Use StatusSeeOther (303) when redirecting after a POST, because it tells the client to switch to GET for the new URL.

The Request Flow

The handler is one node in a chain that starts at the listening socket and ends at the response body bytes. Seeing the chain in one picture makes the surface area clearer.

The server accepts a connection, parses the request, and asks the ServeMux for the handler that matches the path. The mux returns either a struct that implements http.Handler or an http.HandlerFunc (which is a function adapted to the same interface). The handler writes to the ResponseWriter, the server flushes those bytes back to the client, and the connection moves on.

From the handler's point of view, none of the upstream parts exist. The handler just has a request to read and a writer to fill. That's the contract.

A Realistic Handler Struct

When a handler needs to share state (a store, a logger, a config), the struct form pays off. The example below shows a store with two endpoints registered as struct handlers:

Both handlers share the same *ProductStore through their fields. There are no package-level globals, which means a test can build a ProductStore with whatever fixture data it needs and hand it to the handler under test. This is how production Go services are typically wired.

A short refactor note: many teams prefer to skip the struct and use a closure that captures the store:

The closure version is shorter and reads naturally for handlers with one or two dependencies. The struct version scales better when a handler needs many fields, or when you want to attach methods (like middleware-style helpers) to it. Both are idiomatic, pick whichever fits the size of the dependency list.

Common Bugs to Avoid

Two failure modes show up repeatedly in handler code. Both are easy to spot once you know them.

Bug 1: setting headers after `WriteHeader`.

The Content-Type is set after the status was already flushed, so it never reaches the client. The response has Content-Type: text/plain; charset=utf-8 only if Go's auto-sniffing kicks in (it does, on first Write, by inspecting the first 512 bytes of the body). That's not what the code intended, and the bug is invisible unless someone reads the wire.

Fix: set every header before WriteHeader.

Bug 2: forgetting `return` after `http.Error`.

When loadCart fails, http.Error writes the 500 response. Execution continues, and the next line dereferences a nil cart, panicking. Even when it doesn't panic, the extra Fprintln after the error helper corrupts the body.

Fix: every http.Error is followed by return.

A linter like staticcheck flags both of these patterns. Running it on every commit is cheap insurance.

Quiz

HTTP Handlers Quiz

10 quizzes