The cache saves the server's work, but the shop still sends the whole product list every time, even to a browser that got the very same list a minute ago. An ETag fixes that: a short fingerprint of an answer, sent in a header next to it. The same content always gives the same ETag; change one price and it is completely different.
A browser that has a copy sends its ETag back in the If-None-Match header, meaning "send it only if it isn't this one". If the fingerprint still matches, the server answers 304 Not Modified with an empty body, and the browser shows its own copy. A few bytes travel instead of the whole list.
A helper that writes the answer: Go's ServeMux has no hook that runs after every handler, so the ETag work goes in a helper next to writeJSON, and the handler calls the helper to send its answer. A helper can do extra work before it writes; this one, for another job, adds a header:
func writeJSONWithShop(w http.ResponseWriter, v any) {
w.Header().Set("X-Shop", "AlgoShop")
writeJSON(w, 200, v)
}
Your code must give these answers, in this order. The 304s save the trip, not the query: the cache still does that job, so the count stays at 4.
GET /products → 200 ETag: "c375ef8eb42a43606b2b8f96d939d54c725c4af679ed17f288d9477e1649c1dd" [all 8 products, Mug at 12]
GET /products If-None-Match: "c375ef8eb42a43606b2b8f96d939d54c725c4af679ed17f288d9477e1649c1dd" → 304 ETag: "c375ef8eb42a43606b2b8f96d939d54c725c4af679ed17f288d9477e1649c1dd" (empty body)
PATCH /products/1 {"price": 13} → 200 {"id": 1, "price": 13} (no ETag)
GET /products If-None-Match: "c375ef8eb42a43606b2b8f96d939d54c725c4af679ed17f288d9477e1649c1dd" → 200 ETag: "7311ada8ce60ea123df0bd890b6882611182d75d0534227901de6a6d00f5beb9" [all 8 products, Mug at 13]
GET /products If-None-Match: "7311ada8ce60ea123df0bd890b6882611182d75d0534227901de6a6d00f5beb9" → 304 ETag: "7311ada8ce60ea123df0bd890b6882611182d75d0534227901de6a6d00f5beb9" (empty body)
GET /products/99 → 404 {"error": "Product not found"} (no ETag)
GET /debug/queries → 200 {"queries": 4}
Your task: below runExec, write a helper writeJSONWithETag(w, r, v). It turns v into JSON bytes with json.Marshal, hashes them with sha256.Sum256 and sends the hash, in hex between double quotes, as the ETag header. If the request's If-None-Match header holds that same ETag, it answers 304 with no body; otherwise it answers 200 with v. Both answers of the GET /products handler, the cached list and the fresh one, go out through it. The imports crypto/sha256 and fmt are already there.
Assignment
Follow the Coding Tutorial to add ETags and 304 answers to the shop's product list.
Hint%x in fmt.Sprintf writes the hash in hex, and \" puts a double quote inside a Go string. Response headers are set on w.Header(); the browser's header is r.Header.Get("If-None-Match"). On a match, call w.WriteHeader(304) and return before anything else is written.
Free plan: 30 tutor messages. PRO is unlimited.
