How to create HTTP server

Start a Go HTTP server by defining a handler function and calling http.ListenAndServe with your desired port.

You need a URL

You've spent the afternoon building a function that calculates the best pizza toppings based on weather data. It works perfectly in the terminal. Now you need to share it. You don't want to email a script to your friends. You want a URL they can hit from their browser. You need an HTTP server.

Go treats HTTP as a first-class citizen. The standard library includes net/http, which handles the heavy lifting of sockets, headers, and request parsing. You don't need a framework. You write a function that takes a request and writes a response. The library manages the connections. Think of it like a mailroom clerk. The clerk stands by the door, grabs letters, reads the address, and hands them to the right person. In Go, you define who handles which address, and the clerk does the rest.

The standard library handles the wire protocol. You handle the logic.

Minimal server

Here's the simplest server: one handler, one port, blocking call.

package main

import (
	"fmt"
	"net/http"
)

// main starts the HTTP server and blocks until interrupted.
func main() {
	// HandleFunc registers a handler for the root path.
	// The anonymous function runs for every request to "/".
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// WriteHeader sends the status code. 200 is success.
		// Write sends the body. Fprintf combines both.
		fmt.Fprintf(w, "Hello, World!")
	})

	// ListenAndServe starts the server on port 8080.
	// nil tells it to use the default ServeMux (the router).
	// This call blocks forever, keeping the program alive.
	http.ListenAndServe(":8080", nil)
}

Run this code and open http://localhost:8080. You'll see "Hello, World!". The program stays running. It won't exit until you stop it.

What happens under the hood

When you run the program, execution hits http.ListenAndServe and stops moving forward. That function opens a network socket on port 8080 and starts listening. It never returns unless an error occurs. The server is now live.

When a browser hits localhost:8080, the OS delivers the connection to Go. Go wraps the raw TCP data into an http.Request struct. It checks the path. Since the path is /, it calls your anonymous function. Your function writes bytes to the ResponseWriter. Go flushes those bytes back to the client and closes the response. The loop repeats for the next request.

The second argument to ListenAndServe is the handler. Passing nil tells the server to use http.DefaultServeMux. This is a global router. HandleFunc adds routes to this global mux. It works for small apps, but global state can make testing harder. In larger projects, you create a local mux to keep routing isolated.

Realistic handler

Real apps return structured data, set headers, and handle errors. Here's a handler that returns JSON, sets headers, and handles write errors.

// handleUser returns user data as JSON.
// It demonstrates header setting, JSON encoding, and error checks.
func handleUser(w http.ResponseWriter, r *http.Request) {
	// Set content type before writing any body.
	// The client uses this to parse the response correctly.
	w.Header().Set("Content-Type", "application/json")

	// Build the response data.
	user := map[string]string{"name": "Alice", "role": "admin"}

	// Encode the map to JSON and write to the response.
	// Always check the error; the client might have disconnected.
	if err := json.NewEncoder(w).Encode(user); err != nil {
		// Log the error for server-side debugging.
		log.Printf("write error: %v", err)
		// Send a 500 error response to the client.
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
}

This handler follows Go conventions. The parameters are named w and r. This is a community standard. It makes scanning code easier. If you see w, you know it's the response writer. If you see r, you know it's the request.

The ResponseWriter is an interface. It defines three methods: Header(), Write(), and WriteHeader(). Go follows the mantra "accept interfaces, return structs." Your handler accepts the interface, which allows the server to inject the real implementation. This design makes testing easier because you can mock the writer.

Headers must be set before the body. If you call w.Write then w.Header().Set, the headers are already sent to the client. The second set is ignored. The compiler won't catch this. You'll get a bug where the Content-Type is wrong.

Error handling is verbose by design. The if err != nil block makes the unhappy path visible. If the client disconnects mid-response, Encode returns an error. Checking it prevents panics and lets you log what happened.

Pitfalls and errors

The request object is shared. If you spawn a goroutine inside the handler and capture r, you risk a race condition. The server might reuse the Request struct for the next connection while your goroutine is still reading it. Always copy the parts you need, or pass r.Context() instead of the whole request.

Every request carries a context.Context. You access it via r.Context(). This context tracks the request's lifetime. If the client disconnects, the context gets cancelled. Long-running operations should check ctx.Done() to stop early. This prevents goroutine leaks. Always pass the context to database calls or downstream HTTP requests. Context is plumbing. Run it through every long-lived call site.

The router uses patterns. A pattern ending in a slash matches the directory and all sub-paths. A pattern without a slash matches exactly. If you register /api, it matches /api but not /api/users. If you register /api/, it matches /api/ and /api/users. The router redirects requests without the trailing slash to the version with the slash. This behavior catches people off guard. Test your paths carefully.

Compiler errors appear when you misuse types. If you try to pass a function with the wrong signature to mux.Handle, the compiler rejects it with cannot use handler (type func(http.ResponseWriter, *http.Request)) as type http.Handler in argument to mux.Handle. If you forget to check an error return, you get json.NewEncoder(w).Encode(user) evaluated but not used. If you call WriteHeader twice, the server logs http: superfluous response.WriteHeader call.

Run gofmt on your code. The community expects consistent formatting. Most editors do this automatically. Don't argue about indentation; let the tool decide. If you wrap handlers in a struct, name the receiver h or srv. Keep it short. (h *Handler) ServeHTTP is standard.

Headers go first. Errors get checked. The client is always watching.

When to use what

Use http.HandleFunc for quick scripts and prototypes where you need a server in ten lines. Use http.NewServeMux when you want explicit control over routing and plan to add middleware or structured logging. Use a third-party router like chi or gorilla/mux when you need regex path parameters, method restrictions, or a plugin ecosystem. Use httptest when you need to test handlers without starting a real server.

Start simple. Add complexity only when the router forces you to.

Where to go next