Building an HTTP Server
Prerequisites: Making HTTP requests
Go's standard library includes a production-grade HTTP server, so many Go services need no web framework.
- A handler is anything with a
ServeHTTP(http.ResponseWriter, *http.Request)method;http.HandlerFunclets an ordinary function be one. - An
http.ServeMuxroutes requests to handlers. Since Go 1.22, patterns can include a method and wildcards:"GET /items/{id}". Read a wildcard withr.PathValue("id"). - The mux answers
404 Not Foundfor unknown paths and405 Method Not Allowedwhen the path matches but the method doesn't. - Each request runs in its own goroutine, so shared state needs a mutex.
Syntax
mux := http.NewServeMux()
mux.HandleFunc("GET /items/{id}", getItem)
mux.HandleFunc("POST /items", createItem)
func getItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "item %s", id)
}
log.Fatal(http.ListenAndServe(":8080", mux))
Example
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
"time"
)
type Item struct {
ID int `json:"id"`
Name string `json:"name"`
}
type store struct {
mu sync.Mutex
items map[int]Item
nextID int
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json") // headers before WriteHeader
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
log.Println("encode response:", err)
}
}
func (s *store) getItem(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
s.mu.Lock()
item, ok := s.items[id]
s.mu.Unlock()
if !ok {
http.Error(w, "item not found", http.StatusNotFound)
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *store) createItem(w http.ResponseWriter, r *http.Request) {
var in struct {
Name string `json:"name"`
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // reject bodies over 1 MB
if err := json.NewDecoder(r.Body).Decode(&in); err != nil || in.Name == "" {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
s.mu.Lock()
s.nextID++
item := Item{ID: s.nextID, Name: in.Name}
s.items[item.ID] = item
s.mu.Unlock()
writeJSON(w, http.StatusCreated, item)
}
func main() {
s := &store{items: make(map[int]Item)}
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok\n"))
})
mux.HandleFunc("GET /items/{id}", s.getItem)
mux.HandleFunc("POST /items", s.createItem)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
log.Println("listening on", srv.Addr)
log.Fatal(srv.ListenAndServe())
}
Expected output
Run the server with go run ., then in a second terminal:
$ curl localhost:8080/health
ok
$ curl -X POST localhost:8080/items -d '{"name":"pen"}'
{"id":1,"name":"pen"}
$ curl localhost:8080/items/1
{"id":1,"name":"pen"}
$ curl localhost:8080/items/42
item not found
$ curl localhost:8080/items/abc
invalid id
$ curl -X DELETE localhost:8080/items/1
Method Not Allowed
Method-and-wildcard patterns require go 1.22 or later in your go.mod;
with an older version line, "GET /items/{id}" is treated as a literal path.
Common mistakes
- Forgetting
returnafterhttp.Error(...), so the handler carries on and writes a second response. - Setting headers after calling
w.WriteHeaderorw.Write— they are ignored because the headers have already been sent. - Using
http.ListenAndServewith no timeouts for a public server. Anhttp.ServerwithReadTimeout/WriteTimeoutprotects against slow clients. - Modifying shared maps or counters from handlers without a mutex. Handlers run concurrently.
- Registering
"/"and expecting it to match only the home page. A pattern ending in/matches everything below it; use"GET /{$}"for the exact root path.
Practice
Add a DELETE /items/{id} route to the server above. It should respond with
204 No Content when the item existed and 404 when it didn't.
Show solution
func (s *store) deleteItem(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
s.mu.Lock()
_, ok := s.items[id]
delete(s.items, id)
s.mu.Unlock()
if !ok {
http.Error(w, "item not found", http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
}
// In main, next to the other routes:
// mux.HandleFunc("DELETE /items/{id}", s.deleteItem)