The single binary advantage
You wrote a tool to manage Kubernetes pods. It works perfectly on your laptop. You push it to a production server. The server has Python 3.8. Your tool needs 3.11. Or the server is air-gapped and cannot reach PyPI. You spend an hour wrestling with virtual environments, missing shared libraries, and version conflicts. Now imagine the tool is a Go binary. You compile it on your machine, copy the single file to the server, and run it. It just works. No interpreter. No package manager. No "it works on my machine" because the machine doesn't matter.
Go dominates DevOps and infrastructure because it compiles to a single static binary. The compiler bundles your code, the Go runtime, and every dependency into one executable file. The target machine does not need Go installed. It does not need a specific version of Python, Node, or Ruby. It just needs an operating system that supports the binary format. This is called static linking. The binary carries its own luggage.
Cross-compilation is built into the toolchain. You can build a binary for a Linux ARM server while working on a macOS laptop. Set two environment variables and run the build command. The compiler produces the correct machine code without any cross-compiler toolchain setup.
// Build command for a Linux ARM64 binary from any platform.
// GOOS and GOARCH tell the compiler the target operating system and architecture.
// The -ldflags strip debug information and set a version variable for production.
// go build -o my-tool -ldflags="-s -w -X main.version=1.0.0" main.go
Static binaries are the ultimate deployment hack.
Concurrency without the headache
Infrastructure tools often need to do many things at once. You might check the health of fifty services, process logs from multiple sources, or fan out requests to a cluster. In many languages, concurrency means threads. Threads are heavy. They consume megabytes of memory and context switching costs add up. You hit limits quickly.
Go uses goroutines. A goroutine is a lightweight execution unit managed by the Go runtime, not the operating system. The runtime multiplexes millions of goroutines onto a small pool of OS threads. When a goroutine blocks on I/O, the scheduler moves it aside and runs another goroutine on the same thread. This allows high concurrency with minimal memory overhead. Goroutines start with a few kilobytes of stack space and grow as needed.
You launch a goroutine with the go keyword. The function runs concurrently with the caller. If you need to coordinate results, you use channels or synchronization primitives like sync.WaitGroup. The standard library provides everything you need. You rarely reach for third-party concurrency libraries.
Goroutines are cheap. Channels are not magic.
The standard library is your friend
Go ships with a massive standard library. Networking, HTTP, JSON, compression, testing, and OS interaction are all built-in. You can write a production-grade HTTP server, a TLS client, a JSON parser, and a file watcher without importing a single external package. This reduces supply chain risk. You do not need to audit third-party dependencies for security vulnerabilities. It also keeps binaries small and builds fast.
The net/http package is robust. It handles connection pooling, timeouts, and headers correctly. The encoding/json package is fast and safe. The context package manages request lifecycles, deadlines, and cancellation. These packages are designed for infrastructure workloads. They handle edge cases that custom implementations often miss.
Trust the standard library. You rarely need third-party packages for infrastructure.
Realistic example: A concurrent health checker
Infrastructure tools often monitor multiple services. A health checker needs to query several endpoints in parallel, respect timeouts, and report failures. This example shows a realistic pattern using goroutines, context, and synchronization.
package main
import (
"context"
"fmt"
"net/http"
"os"
"sync"
"time"
)
// Service represents a target to monitor.
type Service struct {
Name string
URL string
}
// CheckService performs an HTTP GET request and reports the result.
// It respects the context for cancellation and deadlines.
func CheckService(ctx context.Context, svc Service) error {
// Create a client with a timeout to prevent hanging on slow responses.
client := &http.Client{Timeout: 3 * time.Second}
// Use the context to allow early cancellation if the parent context is done.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.URL, nil)
if err != nil {
return fmt.Errorf("failed to create request for %s: %w", svc.Name, err)
}
// Execute the request.
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request to %s failed: %w", svc.Name, err)
}
// Defer closing the body to prevent connection leaks.
defer resp.Body.Close()
// Check for non-2xx status codes.
if resp.StatusCode >= 400 {
return fmt.Errorf("%s returned status %d", svc.Name, resp.StatusCode)
}
return nil
}
func main() {
services := []Service{
{Name: "API", URL: "http://localhost:8080/health"},
{Name: "DB Proxy", URL: "http://localhost:5432/health"},
{Name: "Cache", URL: "http://localhost:6379/health"},
}
// Create a context with a timeout for the entire check cycle.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var wg sync.WaitGroup
var mu sync.Mutex
var errors []error
// Launch a goroutine for each service to check them in parallel.
for _, svc := range services {
wg.Add(1)
go func(s Service) {
defer wg.Done()
if err := CheckService(ctx, s); err != nil {
// Lock the mutex before modifying the shared errors slice.
mu.Lock()
errors = append(errors, err)
mu.Unlock()
}
}(svc)
}
// Wait for all checks to complete or the context to expire.
wg.Wait()
if len(errors) > 0 {
fmt.Fprintln(os.Stderr, "Health check failures:")
for _, err := range errors {
fmt.Fprintf(os.Stderr, " - %v\n", err)
}
os.Exit(1)
}
fmt.Println("All services healthy")
}
The code creates a context with a five-second deadline. This deadline propagates to every goroutine. If the total time exceeds five seconds, all pending checks cancel immediately. The sync.WaitGroup ensures main waits for all goroutines to finish. The sync.Mutex protects the shared errors slice from data races. The defer resp.Body.Close() call prevents connection leaks. This pattern scales to hundreds of services with minimal code.
Context is plumbing. Run it through every long-lived call site.
Pitfalls and compiler guardrails
Go forces you to handle errors explicitly. You cannot ignore a returned error without assigning it to _. This verbosity is by design. It makes the unhappy path visible. If you try to use a variable that does not exist, the compiler rejects the program with undefined: variable. If you import a package but do not use it, you get imported and not used. These errors save you from runtime surprises.
Goroutine leaks are a common runtime issue. If you launch a goroutine and do not wait for it, it might run indefinitely. The compiler cannot detect this. In a long-running daemon, leaked goroutines accumulate memory and file descriptors. Always provide a cancellation path using context. If a goroutine waits on a channel, ensure the channel gets closed or the context cancels. The worst goroutine bug is the one that never logs.
Binary size can be larger than C, but usually acceptable for infrastructure tools. Go binaries include the runtime and standard library. Use -ldflags="-s -w" to strip debug information and reduce size. For most CLI tools, a few megabytes is negligible compared to the reliability gain.
Convention matters in Go. The gofmt tool formats code automatically. Do not argue about indentation or brace placement. Let the tool decide. Most editors run gofmt on save. Receiver names are usually one or two letters matching the type, like (b *Buffer) Write(...). Public names start with a capital letter. Private names start lowercase. Interfaces are accepted, structs are returned. Follow these conventions and your code will feel idiomatic.
The compiler is your friend. Trust the errors.
When to use Go vs alternatives
Use Go when you need a single static binary that runs anywhere without runtime dependencies. Use Go when your tool requires high concurrency, like checking hundreds of endpoints or processing logs in parallel. Use Go when you want a small dependency footprint and built-in support for HTTP, JSON, and networking. Use Go when you are building CLI tools, daemons, or microservices that need fast startup and low memory usage.
Use Python when you need rapid prototyping, access to a vast ecosystem of data science libraries, or complex scripting with dynamic typing. Use Python when the team is already proficient and the performance requirements are modest.
Use Bash when you are chaining simple existing commands and do not need structured data handling or cross-platform portability. Use Bash for quick one-off scripts that run in a known environment.
Use Rust when you need zero-cost abstractions, fine-grained memory control, or are writing a kernel module or performance-critical library. Use Rust when safety guarantees at compile time are paramount and the learning curve is acceptable.
Pick the tool that matches the job. Go wins on deployment simplicity and concurrency.