Why I love Go
· Astrid Olson
I love Go because it fits how I like to think about software: clear interfaces, explicit behavior, and code that's easy to follow. I appreciate that the language encourages a consistent style and gives me a relatively small set of concepts that compose well. Explicit error handling makes me think through failure cases, and interfaces let me separate responsibilities without building elaborate abstractions. I can move quickly while still writing something I'll be comfortable maintaining—and that another engineer can pick up without needing a tour of every design decision.
I also enjoy how practical Go is for building systems that interact with the outside world. Goroutines, channels, and contexts make concurrency, cancellation, and timeouts natural parts of the design. The standard library and integrated tooling make testing, formatting, profiling, and building feel like one coherent workflow. That combination gives me a satisfying feedback loop: write something, exercise it, understand how it behaves, and improve it. Go makes it easy to focus my attention on the problem I'm solving, which is ultimately what I enjoy most about programming.
Clear boundaries help me move quickly
A lot of the software I work on connects other systems: cloud APIs, Kubernetes resources, deployment tooling, and the commands engineers use to get things done. I want to see where a request starts, what it depends on, and where the result comes back. Small functions and packages with a specific responsibility give me places to ask those questions.
Interfaces are especially useful at those boundaries. A caller can describe the behavior it needs, and a type satisfies that interface through its methods. For example, the part of a tool that needs a service list can depend on a small interface:
type Service struct {
Name string
}
type ServiceSource interface {
List(ctx context.Context) ([]Service, error)
}
func LoadServices(ctx context.Context, source ServiceSource) ([]Service, error) {
services, err := source.List(ctx)
if err != nil {
return nil, fmt.Errorf("list services: %w", err)
}
return services, nil
}This example uses the standard context and fmt packages. The implementation could query an API, while a test could supply a small in-memory source. The interface describes one capability, which makes the relationship easy to read. Go's discussion of interfaces gets at why that kind of composition works well.
Failure belongs in the code I am reading
I like seeing the error path next to the operation that can fail. A failed API call becomes a question I have to answer: can the caller recover, should this operation stop, and what information would help someone understand the failure? That is useful work, especially in infrastructure tooling, where a command can touch several external systems.
In the example, wrapping the error adds the operation being attempted while retaining the underlying error. Go's error wrapping support lets a caller inspect that chain with errors.Is or errors.As. I can add context as the error travels toward the user and decide how to present it at the command or request boundary. That makes the failure easier to follow across packages.
Concurrency comes with a lifetime
External systems take time. Some requests can run independently, some need a deadline, and some should stop when the person who started them cancels the command. Goroutines make concurrent work approachable, and channels give me a way to communicate between those pieces of work. I still need to decide how much work can run at once and who owns its completion.
Contexts help make those decisions visible in function signatures. Passing a context through a call chain gives the operations underneath it access to cancellation and deadlines. The implementation has to cooperate; creating a goroutine does not give it an automatic stopping point. I find that a useful design pressure: I have to think about what happens when the work is no longer needed. The Go team's introduction to context explains how that request-scoped lifetime propagates.
The tooling keeps the feedback loop short
I enjoy how familiar the workflow stays between projects. gofmt handles formatting, go test runs tests, and go build builds the program. Having those tools close at hand makes it easy to check an idea while it is still small. A shared formatting tool also removes a whole set of decisions from code review, leaving more attention for behavior and design.
When I need to understand performance, I can use benchmarks, profiles, and execution traces to investigate what the program is doing. The diagnostics tools give me ways to test an explanation instead of trusting the first optimization that comes to mind. I like being able to move from an observation to a measurement and back to the code within the same workflow.
I want to be comfortable coming back to it
Go fits the kind of maintenance I care about. I want to reopen a package after some time away and recognize its shape. I want a teammate to be able to trace a command through the code, understand its dependencies, and change one part without first reconstructing every assumption I made while writing it.
There is satisfaction in getting that right: a clear interface, a useful error, a background operation with a defined lifetime, and a test that explains the behavior. Go gives me a set of tools I enjoy using to build those things. It leaves me with more attention for the problem, and more confidence that I will understand the solution later.