Open the source of almost any tool you touched today if you work in infrastructure. Kubernetes, Docker, Terraform, Prometheus, Helm, etcd, Vault, Grafana's backend, kubectl plugins, half of the CNCF sandbox. Go, Go, Go. This didn't happen because Google pushed it, or at least not only because of that. It happened because Go's trade-offs line up almost perfectly with what operations work actually needs.
I picked up Go properly during my time as a core engineer at CECG, where it was the house language for platform tooling, and it's now the first thing I reach for when a client needs a tool rather than a script. Here's why I think it won, and where it still loses.
The single binary is the killer feature
Nothing else comes close. When you go build, you get one statically linked file. You copy it to a server, a container, a CI runner, a colleague's laptop, and it runs. No virtualenv, no requirements.txt conflict, no "which Python is this box running", no Ruby gem native extension failing to compile because the image is Alpine and someone forgot build-base.
For ops tooling this is everything. The people running your tool are not the people who wrote it. They are an on-call engineer at 3 a.m., a CI pipeline with a minimal image, or a client's junior admin. Every dependency you make them install is a support ticket you will eventually receive.
Cross-compilation is the same story. GOOS=linux GOARCH=arm64 go build from my desktop and I have a binary for a Graviton instance. I've shipped the same CLI to a client's Intel Macs, their Linux build agents, and an ARM-based edge box, from one Makefile, in one CI job that takes under a minute.
Concurrency that matches the problem
Ops work is embarrassingly concurrent: poll fifty endpoints, tail twelve log streams, fan out an API call across every region, wait for all of it with a timeout. Goroutines and channels map onto that shape directly. You don't need an async runtime, colored functions, or a thread pool tuning exercise. go func(), a sync.WaitGroup or an errgroup, a context with a deadline, done.
Python's asyncio can do all of this, but every time I return to an asyncio codebase after six months I have to relearn it. I have never had to relearn goroutines.
Boring is a feature
Go is a deliberately small language. There's roughly one way to write a loop, error handling is explicit and repetitive, and gofmt ends every formatting argument before it starts. People complain about if err != nil until they inherit a codebase written by someone who left the company two years ago, and then they discover they can read the whole thing in an afternoon.
In consulting this matters more than almost anything. I hand code over. The client's team maintains it after we're gone. A clever Python codebase with metaclasses and decorators three layers deep is a liability on handover day. A dull Go codebase is an asset.
When I reach for what
My honest decision table after a few years of doing this for clients:
| Situation | Tool |
|---|---|
| Glue in a pipeline, < 30 lines, one machine | Bash |
| Data wrangling, one-off analysis, quick API poke | Python |
| Anything long-running, concurrent, or distributed to others | Go |
| Anything a client's team will own after handover | Go |
| Ansible/cloud SDK ecosystems where the libraries live | Python |
Bash gets a hard line: the moment I need an array of structs or error handling beyond set -euo pipefail, it graduates to a real language. Python survives in my toolkit because boto3 and the data ecosystem are genuinely better than the Go equivalents for exploration. But the moment a script becomes a tool, something with a flag parser, a README, and users who aren't me, it gets rewritten in Go.
A real example: reconciling S3 lifecycle rules
A client had around ninety S3 buckets across three accounts, created over years by different teams, and wanted a guarantee that every bucket matching certain prefixes had the correct lifecycle policy. The first version was a Python script. It worked, until it had to run in a locked-down CI image with no pip access, and until it needed to check all three accounts in parallel to finish inside the pipeline timeout.
The Go rewrite is about 300 lines. The core of it:
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)
for _, b := range buckets {
b := b
g.Go(func() error {
current, err := getLifecycle(ctx, client, b.Name)
if err != nil {
return fmt.Errorf("bucket %s: %w", b.Name, err)
}
if diff := compare(current, desired(b)); diff != "" {
report(b.Name, diff)
if apply {
return putLifecycle(ctx, client, b.Name, desired(b))
}
}
return nil
})
}
return g.Wait()
Dry-run by default, --apply to mutate, one binary in the CI image, ninety buckets checked in a few seconds. The client's team has since extended it to check bucket encryption and public access blocks, without asking us for help. That last part is the point.
Advice for ops people learning Go
Skip the web framework tutorials. You don't need Gin or a REST API to start. Your first Go program should be a rewrite of a shell script you already own. You know the requirements, you know the edge cases, and you'll feel the difference immediately.
Learn the standard library before any dependency. os/exec, flag, net/http, encoding/json, context, time. That's 80% of ops tooling. The Go standard library is good in a way that Python's mostly isn't for this kind of work.
Embrace the error handling instead of fighting it. Wrap errors with context (fmt.Errorf("reading config %s: %w", path, err)) at every level. When your tool fails in someone else's pipeline, that chain of context is the difference between a five-minute fix and a screen-share.
Read real tools. The source of kubectl plugins, cobra-based CLIs, or something small like direnv will teach you idiomatic structure faster than any course.
Don't over-engineer. Ops Go should look almost naive: a main.go, maybe two or three packages, no interfaces until a second implementation actually exists. The Java refugees writing AbstractBucketReconcilerFactory in Go are missing the entire point of the language.
Go won DevOps because infrastructure work rewards tools that are easy to distribute, easy to read, and hard to break, more than it rewards expressiveness. It's not the most exciting language I've used. That's exactly why I keep using it.