go
59 lines · 9 steps
Optimistic concurrency with If-Match in Gin
A Gin handler that uses the If-Match header and version numbers to reject lost-update writes.
Explained by
highlit
1func UpdateArticle(store *ArticleStore) gin.HandlerFunc {
2 return func(c *gin.Context) {
3 id := c.Param("id")
4
5 ifMatch := strings.Trim(c.GetHeader("If-Match"), `"`)
6 if ifMatch == "" {
7 c.JSON(http.StatusPreconditionRequired, gin.H{"error": "If-Match header is required"})
8 return
9 }
10
11 expectedVersion, err := strconv.ParseInt(ifMatch, 10, 64)
12 if err != nil {
13 c.JSON(http.StatusBadRequest, gin.H{"error": "invalid If-Match version"})
14 return
15 }
16
17 var payload struct {
18 Title string `json:"title" binding:"required"`
19 Body string `json:"body" binding:"required"`
20 }
21 if err := c.ShouldBindJSON(&payload); err != nil {
22 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
23 return
24 }
25
26 article, err := store.FindByID(c.Request.Context(), id)
27 if errors.Is(err, ErrNotFound) {
28 c.JSON(http.StatusNotFound, gin.H{"error": "article not found"})
29 return
30 } else if err != nil {
31 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load article"})
32 return
33 }
34
35 if article.Version != expectedVersion {
36 c.Header("ETag", strconv.FormatInt(article.Version, 10))
37 c.JSON(http.StatusPreconditionFailed, gin.H{
38 "error": "article was modified by another request",
39 "current_version": article.Version,
40 })
41 return
42 }
43
44 article.Title = payload.Title
45 article.Body = payload.Body
46
47 updated, err := store.UpdateWithVersion(c.Request.Context(), article, expectedVersion)
48 if errors.Is(err, ErrVersionConflict) {
49 c.JSON(http.StatusPreconditionFailed, gin.H{"error": "article was modified concurrently"})
50 return
51 } else if err != nil {
52 c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update article"})
53 return
54 }
55
56 c.Header("ETag", strconv.FormatInt(updated.Version, 10))
57 c.JSON(http.StatusOK, updated)
58 }
59}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1The If-Match header carries a client's expected version so the server can reject stale writes.
- 2Comparing versions before and during the update guards against lost updates from concurrent requests.
- 3Returning the current version in a 412 response lets clients retry with fresh state.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/optimistic-concurrency-with-if-match-in-gin-explained-go-1bdc/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.