Canary Deployments with Go: How to Release Updates Without Fear and Risk
Imagine: you've spent years building the architecture, writing tests, setting up monitoring. And then — the release. One click, and the new version goes to production. Your heart is pounding, your fingers are shaking. Sound familiar? What if I told you that you can deploy a new version to just 5% of users, observe the metrics, and only then roll it out to everyone? Without downtime, without panic, with full control.
This is called a canary deployment. The name comes from miners who took canaries with them — the bird would die from gas before the humans, warning of danger. In the DevOps world, these "canaries" are the first 5-10% of users who receive the new version. If something goes wrong — we roll back without anyone noticing.
In this article, we'll break down how to implement canary deployment in Go with real code examples, a load balancer, and metrics. Let's go!
Why Go for Canary Deployment?
Go is a language built for microservices. It compiles to a static binary, requires no dependencies on the server, and scales easily. But most importantly, Go has built-in concurrency support and excellent libraries for HTTP servers, making it an ideal candidate for implementing smart traffic routing.
- Performance — A Go server handles thousands of requests per second on a single core.
- Simplicity of deployment — One binary, no virtual machines or interpreters.
- Rich ecosystem — Packages for metrics (Prometheus), tracing (OpenTelemetry), and load balancing.
If you're already writing in Go and want to automate deployment — the canary strategy will become your best friend.
Canary Deployment Architecture in Go
Let's imagine we have a microservice in Go that processes user requests. Usually, we deploy a new version to all instances at once. With the canary approach, we run the new version in parallel with the old one, but only direct a portion of traffic to it.
Here's a simplified diagram:
- Stable version (v1) — runs on 90% of instances.
- Canary version (v2) — runs on 10% of instances.
- Load balancer — distributes traffic proportionally.
- Monitoring — compares metrics (latency, errors, throughput).
- Manual or automatic decision — roll out v2 to 100% or roll back.
This can be implemented in various ways: via Kubernetes (Service Mesh), via Nginx with custom weights, or by writing your own load balancer in Go. We'll go with the third approach — it gives full control and is great for learning.
Writing a Canary Traffic Load Balancer in Go
Let's write a simple reverse-proxy that will route requests to two backends: stable and canary. We'll set the canary version's weight via a config or environment variable.
package main
import ( "log" "math/rand" "net/http" "net/http/httputil" "net/url" "os" "strconv")
type CanaryProxy struct { stable *httputil.ReverseProxy canary *httputil.ReverseProxy canaryWeight int // percentage of traffic to canary (0-100)}
func NewCanaryProxy(stableURL, canaryURL string, weight int) *CanaryProxy { stableTarget, _ := url.Parse(stableURL) canaryTarget, _ := url.Parse(canaryURL)
return &CanaryProxy{ stable: httputil.NewSingleHostReverseProxy(stableTarget), canary: httputil.NewSingleHostReverseProxy(canaryTarget), canaryWeight: weight, }}
func (p *CanaryProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Decision based on a random number if rand.Intn(100) <