Worked review 1: HTTP JSON API handler
This is a small task-list HTTP API: POST /tasks creates a task, GET /tasks/{id} fetches one. It is written in the confident, plausible style an AI assistant produces on the first try — it compiles, go vet says nothing, and it runs. Read it before running it. It has 5 real bugs.
The program
Section titled “The program”package main
import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strconv" "strings")
type Task struct { ID int `json:"id"` Text string `json:"text"`}
type Store struct { tasks map[int]Task nextID int}
func NewStore() *Store { return &Store{tasks: map[int]Task{}, nextID: 1}}
func (s Store) Add(text string) Task { t := Task{ID: s.nextID, Text: text} s.tasks[t.ID] = t s.nextID++ return t}
func (s *Store) Get(id int) (Task, bool) { t, ok := s.tasks[id] return t, ok}
func newMux(store *Store) *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) { var body struct { Text string `json:"text"` } json.NewDecoder(r.Body).Decode(&body) t := store.Add(body.Text) json.NewEncoder(w).Encode(t) w.WriteHeader(http.StatusCreated) }) mux.HandleFunc("GET /tasks/{id}", func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(r.PathValue("id")) t, ok := store.Get(id) if !ok { w.WriteHeader(http.StatusNotFound) } json.NewEncoder(w).Encode(t) }) return mux}
func main() { store := NewStore() ts := httptest.NewServer(newMux(store)) defer ts.Close()
for _, text := range []string{"buy milk", "write report"} { payload := strings.NewReader(fmt.Sprintf(`{"text":%q}`, text)) resp, _ := http.Post(ts.URL+"/tasks", "application/json", payload) body, _ := io.ReadAll(resp.Body) fmt.Printf("POST /tasks status=%d body=%s\n", resp.StatusCode, body) }
resp, _ := http.Post(ts.URL+"/tasks", "application/json", strings.NewReader("not json")) body, _ := io.ReadAll(resp.Body) fmt.Printf("POST /tasks (bad json) status=%d body=%s\n", resp.StatusCode, body)
resp, _ = http.Get(ts.URL + "/tasks/abc") body, _ = io.ReadAll(resp.Body) fmt.Printf("GET /tasks/abc status=%d body=%s\n", resp.StatusCode, body)
resp, _ = http.Get(ts.URL + "/tasks/999") body, _ = io.ReadAll(resp.Body) fmt.Printf("GET /tasks/999 status=%d body=%s\n", resp.StatusCode, body)} Find the bugs
Section titled “Find the bugs”Run it. Every response comes back with status=200, one task’s id never changes, and a 404 has a body attached to it. None of that is intentional. Before opening the answer below, try to name at least three of the five bugs and which line causes each.
Reveal the 5 bugs
func (s Store) Add(...)has a value receiver.Addmutatess.nextIDand writes intos.tasks— butsis a full copy ofStoreon every call. The map write survives (a map value is a shared reference), buts.nextID++only increments the copy; the realStore.nextIDnever moves past 1. Every task created gets"id":1, silently colliding in the map. Real output: bothbuy milkandwrite reportcome back as{"id":1,...}— the second call overwrote the first instore.tasks.json.NewDecoder(r.Body).Decode(&body)’s error is ignored. POSTing"not json"doesn’t fail —body.Textjust stays"", and the handler happily creates a task with empty text and returns 200.w.WriteHeader(http.StatusCreated)is called afterjson.NewEncoder(w).Encode(t).EncodecallsWriteinternally, and the firstWriteon aResponseWriterimplicitly sends a200status ifWriteHeaderhasn’t been called yet. By the timeWriteHeader(201)runs, the response is already sent —go runlogshttp: superfluous response.WriteHeader calland the client sees200, never201.- The 404 branch does not
return.if !ok { w.WriteHeader(http.StatusNotFound) }sends the 404 header, then execution falls through tojson.NewEncoder(w).Encode(t)anyway — encoding the zero-valueTask{}.GET /tasks/999comes back404with a body of{"id":0,"text":""}, which looks like a malformed but existing task rather than “not found.” strconv.Atoi(r.PathValue("id"))’s error is ignored.GET /tasks/abcdoesn’t return400 Bad Request—idsilently becomes0, and the handler proceeds to look up task0as if that were a normal, valid request.
The corrected program
Section titled “The corrected program”package main
import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strconv" "strings" "sync")
type Task struct { ID int `json:"id"` Text string `json:"text"`}
type Store struct { mu sync.Mutex // fixed: guards tasks/nextID against concurrent handlers tasks map[int]Task nextID int}
func NewStore() *Store { return &Store{tasks: map[int]Task{}, nextID: 1}}
func (s *Store) Add(text string) Task { // fixed: pointer receiver — nextID actually persists s.mu.Lock() defer s.mu.Unlock() t := Task{ID: s.nextID, Text: text} s.tasks[t.ID] = t s.nextID++ return t}
func (s *Store) Get(id int) (Task, bool) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.tasks[id] return t, ok}
func newMux(store *Store) *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) { var body struct { Text string `json:"text"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid JSON body", http.StatusBadRequest) return } t := store.Add(body.Text) w.WriteHeader(http.StatusCreated) // fixed: header before the first Write _ = json.NewEncoder(w).Encode(t) }) mux.HandleFunc("GET /tasks/{id}", func(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil { http.Error(w, "invalid id", http.StatusBadRequest) return } t, ok := store.Get(id) if !ok { w.WriteHeader(http.StatusNotFound) return // fixed: stop here, do not also write a zero-value body } _ = json.NewEncoder(w).Encode(t) }) return mux}
func main() { store := NewStore() ts := httptest.NewServer(newMux(store)) defer ts.Close()
for _, text := range []string{"buy milk", "write report"} { payload := strings.NewReader(fmt.Sprintf(`{"text":%q}`, text)) resp, _ := http.Post(ts.URL+"/tasks", "application/json", payload) body, _ := io.ReadAll(resp.Body) fmt.Printf("POST /tasks status=%d body=%s\n", resp.StatusCode, body) }
resp, _ := http.Post(ts.URL+"/tasks", "application/json", strings.NewReader("not json")) body, _ := io.ReadAll(resp.Body) fmt.Printf("POST /tasks (bad json) status=%d body=%s\n", resp.StatusCode, body)
resp, _ = http.Get(ts.URL + "/tasks/abc") body, _ = io.ReadAll(resp.Body) fmt.Printf("GET /tasks/abc status=%d body=%s\n", resp.StatusCode, body)
resp, _ = http.Get(ts.URL + "/tasks/999") body, _ = io.ReadAll(resp.Body) fmt.Printf("GET /tasks/999 status=%d body=%s\n", resp.StatusCode, body)} Run the corrected version: IDs increment (1, then 2), a malformed body now gets 400 invalid JSON body, /tasks/abc gets 400 invalid id instead of a false 404, and /tasks/999 gets a real 404 with an empty body. go vet and golangci-lint run both report zero issues on this version — the added sync.Mutex also closes the concurrency-safety gap a pointer-receiver Store would otherwise still have under real concurrent traffic (this demo’s requests run sequentially, so the original wouldn’t have shown a race here, but production traffic would).