Skip to content

Expert · Standard library essentials · topic 31 of 46 · 45 min

An HTTP server with net/http

Explanation

net/http gives you a production-grade web server with no extra libraries. A handler is a function that receives a request and writes a response. Since Go 1.22, http.ServeMux understands methods and path wildcards, like "GET /items/{id}", and r.PathValue("id") reads the value. In production, start the server through an http.Server with timeouts.

How a web request works

A client (a browser, a mobile app, curl) sends an HTTP request: a method such as GET or POST, a path such as /items/42, headers, and sometimes a body. The server sends back a response: a status code (200 OK, 404 Not Found, 500 Internal Server Error), headers, and a body, often JSON. In Go, each incoming request is handled in its own goroutine, so many users are served at once.

The smallest server

go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Namaste from Go!")
	})
	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
Open http://localhost:8080/hello in a browser. ListenAndServe only returns on an error, which log.Fatal prints before exiting.
  • http.ResponseWriter w: where you write the response (it is an io.Writer, so fmt.Fprintln works).
  • *http.Request r: everything about the request: method, URL, headers, body, context.
  • A handler is any value with a ServeHTTP(w, r) method; http.HandlerFunc turns a plain function into one.
  • nil as the second argument means "use the default ServeMux". In real apps create your own with http.NewServeMux().

Routing with Go 1.22 patterns

go
mux := http.NewServeMux()
mux.HandleFunc("GET /items", listItems)
mux.HandleFunc("GET /items/{id}", getItem)
mux.HandleFunc("POST /items", createItem)
mux.HandleFunc("DELETE /items/{id}", deleteItem)
mux.HandleFunc("GET /{$}", home)          // exactly "/" and nothing else
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))

func getItem(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")               // the {id} part, as a string
	fmt.Fprintf(w, "item %s\n", id)
}
Method and wildcard patterns need Go 1.22 or newer (and go 1.22+ in go.mod).
PatternMatches
"GET /items/{id}"GET (and HEAD) /items/42, /items/abc; not /items/42/x
"/static/"Anything starting with /static/ (a trailing slash means a whole subtree)
"/{$}"Only the root path /
"/files/{path...}"The rest of the path, including slashes, in one wildcard

If the path matches but the method does not (a POST to "GET /items"), the mux answers 405 Method Not Allowed for you. If two patterns overlap, the more specific one wins; if neither is more specific, registering them panics at start-up, so conflicts are caught early. Before Go 1.22 people used third-party routers such as gorilla/mux or chi for this; many projects still use chi, which is fine.

Reading the request

go
q := r.URL.Query().Get("q")            // ?q=dosa
page := r.URL.Query().Get("page")      // "" if missing
auth := r.Header.Get("Authorization")
ct := r.Header.Get("Content-Type")
// r.Body is an io.ReadCloser: decode JSON from it (next topics)
Query values and headers are strings; convert and validate them yourself.

Writing the response: order matters

go
func createItem(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json") // 1. headers
	w.WriteHeader(http.StatusCreated)                    // 2. status (201)
	w.Write([]byte(`{"id":42}`))                       // 3. body
}

func missing(w http.ResponseWriter, r *http.Request) {
	http.Error(w, "item not found", http.StatusNotFound) // plain-text error
}
Set headers, then the status, then write the body. Headers changed after WriteHeader (or after the first Write) are ignored. Writing without WriteHeader means 200 OK.

Production: http.Server with timeouts

go
srv := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 5 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      15 * time.Second,
	IdleTimeout:       60 * time.Second,
}
log.Fatal(srv.ListenAndServe())
http.ListenAndServe has no timeouts at all. A slow or malicious client could hold connections open forever (a "slowloris" attack).
Careful: Common mistakes: forgetting to return after http.Error (the handler keeps running and writes more), calling w.WriteHeader twice (Go logs "superfluous response.WriteHeader call"), and using the global default mux in big apps where any imported package can register routes on it.
Tip: Test a server from the terminal with curl: curl -i localhost:8080/items/42 shows the status line and headers; curl -X POST -d '{"name":"chai"}' -H "Content-Type: application/json" localhost:8080/items sends JSON.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

An HTTP server with net/http · 1/7

net/http server

Go's standard library includes a production-grade web server: net slash http.

Prefer a video by a teacher? Find free videos on “golang net/http server go 1.22 routing servemux pathvalue” on YouTube ↗

In real life: A canteen menu service

A college canteen shows its menu on a screen and in a small app. A Go service with three routes is enough: list the menu, get one item, and a health check that the hosting platform calls every few seconds. It is one binary, uses a few MB of memory, and handles every student's phone at lunch time.

go
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

var menu = map[string]int{"dosa": 40, "idli": 30, "chai": 15}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "ok")
	})
	mux.HandleFunc("GET /menu/{item}", func(w http.ResponseWriter, r *http.Request) {
		item := r.PathValue("item")
		price, ok := menu[item]
		if !ok {
			http.Error(w, "no such item", http.StatusNotFound)
			return
		}
		fmt.Fprintf(w, "%s: Rs %d\n", item, price)
	})

	srv := &http.Server{Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
	log.Fatal(srv.ListenAndServe())
}
curl localhost:8080/menu/dosa prints dosa: Rs 40; /menu/pizza gives 404. The map is only read, so it is safe to share between request goroutines.

Practice (0/4 done)

Task 1 · Question

With mux.HandleFunc("GET /users/{id}", h), what does r.PathValue("id") return for GET /users/77?

Task 2 · Question

A client sends POST /users/77 but only "GET /users/{id}" is registered. What does ServeMux reply?

Task 3 · Question

Which line is the bug? 1: if item == "" { 2: http.Error(w, "missing item", http.StatusBadRequest) 3: } 4: fmt.Fprintf(w, "item: %s", item)

Task 4 · Practice

Build a server with an http.Server that has timeouts and three routes: GET /healthz returns "ok"; GET /greet/{name} returns "Namaste, NAME"; GET /square?n=5 returns 25, or 400 Bad Request if n is not a number. Test each route with curl -i.

Quick quiz

Question 1

1.How does net/http handle many requests at once?

Question 2

2.Which pattern matches only the root path "/"?

Question 3

3.What status is sent if you call w.Write without w.WriteHeader?

Question 4

4.Why use http.Server with timeouts instead of http.ListenAndServe?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

How does net/http handle many requests at once?

Answer: Each request runs in its own goroutine

The server starts a goroutine per connection/request.

Which pattern matches only the root path "/"?

Answer: "/{$}"

"/" alone matches everything; "/{$}" matches exactly "/".

What status is sent if you call w.Write without w.WriteHeader?

Answer: 200 OK

The first Write sends an implicit 200 OK.

Why use http.Server with timeouts instead of http.ListenAndServe?

Answer: ListenAndServe has no timeouts, so slow clients can hold connections forever

Timeouts protect the server from slow or malicious clients.

HTTP is…Related

Answer: The text-based language browsers and servers use

HTTP is the request/response protocol of the web.

From How web requests work (and why that matters)

Which part of an HTTP request carries the JSON data you send?Related

Answer: The body

The body comes after the headers and the empty line, and holds the data.

From HTTP requests, responses and methods

A server replies 404 Not Found. What does the fetch promise do?Related

Answer: Fulfils with a Response whose ok is false

fetch only rejects on network failure.

From fetch: talking to servers

Hundreds of 404 responses for paths like /backup.zip, /.git/config and /admin.php from one IP suggest…Related

Answer: Someone probing for hidden files

Automated scanners try many common hidden paths, producing many 404s.

From HTTP, HTTPS and TLS

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Standard library essentials” before its exam: Calling other APIs safely with the HTTP client, JSON with encoding/json, Files, time and structured logs.
  2. Module 7 exam: Standard library essentials · 14 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Building a REST API with a database. 46 topics left in the course before the final exam.

← Previous: context: timeouts and cancellation