← Go, End to End

Go, End to End

Building a Minimal HTTP Server

This is meant to be a genuinely practical walkthrough, not just syntax. Here's a real, complete, minimal HTTP server using only the standard library.

The basics: ServeMux and handlers

A handler is anything satisfying http.Handler (a ServeHTTP(w http.ResponseWriter, r *http.Request) method), but in practice you'll mostly write plain functions and register them with http.HandleFunc, which wraps them into handlers for you:

go · live, editable, runnable

This example is meant to illustrate the pattern and won't let you actually curl it from this embedded panel (no real network access in the sandbox). Run it locally with http.ListenAndServe(":8080", nil) uncommented at the end of main, then visit http://localhost:8080/hello?name=World in a browser to see it respond for real.

A slightly more complete example

Real handlers usually need to inspect the method, read the URL path, and write a proper status code. http.ResponseWriter's WriteHeader sets the status; if you never call it, the first Write implicitly sends a 200.

go
package main

import (
	"encoding/json"
	"net/http"
)

type Health struct {
	Status string `json:"status"`
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(Health{Status: "ok"})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/health", healthHandler)
	http.ListenAndServe(":8080", mux)
}

Using an explicit http.NewServeMux() instead of the default global mux (the nil in the first example implicitly uses http.DefaultServeMux) is the better habit once a program has more than a couple routes: it avoids accidental route collisions between packages that both register against the global default.

The flag package for CLI configuration

Servers usually need a configurable port or a few other startup options. flag handles this without a third-party CLI framework:

go
var port = flag.Int("port", 8080, "port to listen on")

func main() {
	flag.Parse()
	addr := fmt.Sprintf(":%d", *port)
	http.ListenAndServe(addr, nil)
}

Run it as ./server -port 9090 and *port reflects the flag value (or the default, 8080, if the flag wasn't passed). flag is enough for straightforward tools; genuinely complex CLIs with subcommands often reach for a third-party library like cobra, but that's a scale decision, not a default.

Try it yourself: extend the health-check handler to also return the current time in the JSON response, then run it locally and hit it with curl.