This POC (Proof of Concept) demonstrates why using defer with mutexes is a bad practice that can drastically impact the performance of a Go application under concurrent load.
Many Go developers systematically use defer to unlock mutexes:
mu.Lock()
defer mu.Unlock() // ❌ BAD PRACTICE
// ... long processing ...This approach keeps the mutex locked for the entire duration of the function, creating a critical bottleneck.
Unlock the mutex immediately after the critical section:
mu.Lock()
// ... fast critical operation ...
mu.Unlock() // ✅ GOOD PRACTICE
// ... long processing WITHOUT the mutex ...The tests compare two identical HTTP servers, with the only difference being how mutexes are handled:
| Concurrency | Bad Server (defer) | Good Server | Improvement |
|---|---|---|---|
| 1 goroutine | 11.12 ms | 11.20 ms | -0.7% |
| 10 goroutines | 106.97 ms | 11.89 ms | 88.9% |
| 50 goroutines | 419.25 ms | 13.84 ms | 96.7% |
| 100 goroutines | 558.35 ms | 16.17 ms | 97.1% |
- Bad Server: ~87–90 req/s (constant, regardless of concurrency)
- Good Server:
- 1 goroutine: 88 req/s
- 10 goroutines: 687 req/s
- 50 goroutines: 367 req/s
- 100 goroutines: 316 req/s
- No concurrency (1 goroutine): Same performance, no contention
- With concurrency: The "bad" server becomes a bottleneck, as only one goroutine can proceed at a time
- Exponential impact: The higher the concurrency, the worse the degradation (up to 97% slower)
bad_server.go: HTTP server using mutex + defer (port 8081)good_server.go: HTTP server with optimized mutex usage (port 8082)benchmark_test.go: Comparative load testsrun_benchmark.sh: Benchmark automation script
- Go 1.21 or higher
- Git (to clone the project)
- Clone the repository:
git clone <repo-url>
cd poc- Install dependencies:
go mod download
go mod tidyThe run_benchmark.sh script automatically handles server startup and benchmarking:
chmod +x run_benchmark.sh
./run_benchmark.shThe script will:
- Start both servers in the background
- Check that they respond properly
- Run each benchmark for 10 seconds
- Display comparative latency results
- Gracefully stop the servers
If you prefer to control each step:
- Terminal 1 – Start the "bad" server:
go run bad_server.go
# Server listens on http://localhost:8081- Terminal 2 – Start the "good" server:
go run good_server.go
# Server listens on http://localhost:8082- Terminal 3 – Verify that servers are running:
# Test the "bad" server
curl http://localhost:8081/stats
# Test the "good" server
curl http://localhost:8082/stats- Terminal 3 – Run the benchmarks:
# Full benchmarks (10 seconds per test)
go test -bench=. -benchtime=10s benchmark_test.go
# Quick version (1 second per test)
go test -bench=. -benchtime=1s benchmark_test.go
# Latency-only test
go test -run TestLatencyComparison -v benchmark_test.goThe benchmarks output:
- ns/op: Nanoseconds per operation
- ms/req: Milliseconds per request (easier to read)
- req/s: Requests per second (throughput)
As concurrency increases, the difference between the two approaches becomes more apparent.
- Only use
deferwith mutexes for very short operations - Release mutexes as soon as possible to enable parallelism
- Copy required data, then unlock the mutex before processing
- Performance impact can be catastrophic (up to 97% degradation)
This POC proves that systematically using defer with mutexes is an anti-pattern that can effectively turn your application into a single-threaded system, negating all the benefits of Go's concurrency model.