Go in the Trenches

After years of writing production code in other languages, I finally had the chance to build a serious service from scratch in Go. Coming in with fresh eyes, I kept a running log of what worked, what didn't, and what surprised me about the language. The verdict is mostly positive, but not without real reservations.

The Core Strengths

Verbose by Design

Go requires you to type a lot compared to many other languages. There are few shortcuts, and the language makes no apologies for that. The payoff comes later: code written this way is eminently readable. With languages like Ruby, Lisp, or C++, understanding a project often means deciphering the bespoke abstractions each developer layered on to reduce line count. Go's verbosity eliminates that cognitive overhead almost entirely.

Concurrency Done Right

After real work with Goroutines and channels, I'm convinced they represent the gold standard for exposing concurrency to developers. Working with concurrency in languages like Ruby is frustrating not because concurrency is inherently hard, but because the primitives are dull and error-prone by default. In Go, concurrent code frequently works perfectly the first time. When it doesn't, the problem is usually my conceptual mistake, not a poor language feature.

Go's opinionated stance matters here. Haskell and Rust hand you every concurrency primitive under the sun. That freedom fragments the ecosystem and leaves no clear idiomatic path forward. Go's restraint is a feature.

Speed at Every Level

Runtime performance matters, but the tooling speed is what transforms the development experience. Compiling and running the entire test suite in under a second changes how you work in a way that's hard to overstate. Going back to 10-second iteration loops in Ruby feels like running a marathon through mud. Other modern languages obsess over runtime speed or fancy features while ignoring the feedback cycle entirely.

And yes, Go runs fast too. Writing in a high-level language and trusting it to perform well is genuinely pleasant.

Deployment Simplicity

If all languages deployed this easily, Docker's popularity would have been diminished significantly. Build a binary, copy it to the server, restart the service. No environment drift, no dependency resolution headaches, no bundler. For the same reason, I now write my throwaway scripts in Go — if I need to run one via Cron, I copy the executable to /usr/local/bin, add it to the crontab, and I'm done. No $PATH issues, no version managers to deal with. Even killall just works.

What Works Well

  • Defer: A clean abstraction for cleanup. Not as safe as C#'s using block, but far less cluttering.
  • Import conventions: Canonical short identifiers like fmt are the One True Way. No more hunting for the origin of a symbol, no mix of qualified and unqualified names.
  • Select: Powerful construct, even if the default: blocking semantics take some getting used to.
  • Pipelines: Combining language features to build composable parallelism is elegant and encourages proper use of multicore systems.
  • Labels and goto: Labels make breaking out of outer loops trivial. goto comes with just enough restrictions to prevent abuse.
  • No metaprogramming, minimal OO: I'll write more code if it means someone else can actually understand it.
  • Static linking by default: Introduces a few headaches, but vastly improves life for everyone else.
  • Standard library in Go: Being able to read the implementation of core packages is invaluable — and increasingly damning for languages that still write their standard libraries in C.
  • Documentation tooling: A locally runnable doc server and testable examples that run with the test suite solve real problems seen in most other languages.

Surprising Wins

  • Dependency management: I resisted this design initially, but skipping slow, complex package managers massively improves the development experience — and makes diving into third-party libraries trivial.
  • Gofmt: One convention for everything makes collaboration easier and speeds up my own coding.
  • Unused variable errors: Annoying in the moment, but these have saved me from real bugs multiple times.
  • No generics: For a multi-thousand-line program, I never once wished for them. Typed slices and maps get you surprisingly far.

Genuine Frustrations

  • Error handling: My programs rarely crash, but the cost is an incredible level of micro-management. The pattern of passing errors through return values can make the original error site genuinely difficult to find.
  • The community: Mailing list discussions remain dispiriting. Every critique, no matter how valid, meets a barrage of dismissive responses. Such zealotry was previously reserved for text editor wars.
  • Debugging: gdb and pprof work, but roughly enough around the edges that I frequently resort to print-debugging to avoid the hassle.
  • Noisy diffs: Adding a field with a long name to a large struct reformats all the struct's spacing, producing a sea of red in code review.
  • Quirky semantics: Many are fine once learned, but unnecessarily opaque at first. The distinction between new, make, and composite literals; interfaces always being references; exported symbols starting with capital letters; unbuffered channels blocking; select with default becoming non-blocking; the rare second return value for map key checks; named return values; closed channels falling through with zero values; and comparing interfaces to nil being allowed but dangerous — each one is a small trap.
  • JSON performance: The reflection-heavy implementation is as slow as widely reported, capable of surprising bottlenecks in otherwise fast programs.

Room for Improvement

The most significant problem is testing. The absence of meaningful assert functions means writing a custom message every time you want to verify an error is nil. That's not merely inconvenient — the resulting verbosity actively deters developers from writing tests, and projects with poor test suites are common as a result. Third-party packages like the testify require package help, but the standard library should provide a proper answer.

Go has its rough edges, but the core experience — readable code, sane concurrency, fast iteration, easy deployment — makes those frustrations worth wrestling with.

Design Trade-offs

Go is simple, but that simplicity comes at the price of verbosity. The language is deliberately small, and much of what other ecosystems handle through frameworks or metaprogramming is done here by hand. Error handling is the most visible example: there is no exception mechanism, so functions return errors explicitly and callers must check them at each step. The result is repetitive but unambiguous code that makes the flow of failure obvious.

The type system is similarly modest. There are no generics in the classic sense, no inheritance, and no operator overloading. Instead, Go relies on interfaces and composition. This keeps the mental model small: code reads top to bottom, and behavior is determined by what a type does, not what it extends. The compiler catches many whole classes of mistakes early — unused variables, misused pointers, accidental type mismatches — while still leaving enough room to move that experienced developers can be productive within a week.

This compromise, between compile-time safety and fluidity of development, is arguably Go's best achievement. Stronger languages like Rust or Haskell offer deeper guarantees, but they demand a much steeper learning curve. Weaker languages allow faster iteration at the cost of deferred debugging. Go sits somewhere unique: constraints that find real bugs at compile time, with a syntax that never gets in the way of expressing a straightforward algorithm.

Exceptional Areas

Where Go really shines is in three areas: speed, deployment, and — above all — concurrency. Goroutines are lightweight, cheap to spawn in the hundreds of thousands, and channel-based communication makes coordination explicit and readable. The model is coherent enough that beginners pick up the basics quickly, while still being expressive for more complex designs. It is the closest mainstream approximation to the CSP model without needing contrived workarounds1.

Compilation and execution speed are frequently praised, and rightly so. Builds are fast even for large repositories, and the static binary that emerges carries all its dependencies inside. With GOOS and GOARCH set appropriately, cross-compilation for Linux, Windows, and macOS is trivial. This makes distribution nearly frictionless: copy a single executable over SSH, and it runs without runtime installation or library matching.

Maintenance Nitpicks

The lack of generics leads to code duplication that no amount of interface trickery fully hides. Most noticeable when writing collection helpers, or data structures that should work for multiple types, it pushes programmers toward code generation or copy-paste. Similarly, enumerations are awkward to define properly — the recommended patterns are either error-prone constants or verbose structs that are easy to misuse.

Dependency management was a sore spot for years. While tools like govendor and later go mod addressed reproducible builds reasonably well, the transition from the original GOPATH model caused confusion. Older documentation still references the deprecated structure, and tutorials vary widely in approach. This is churn rather than a fundamental flaw, but it is a practical cost each new project must still pay.

A less obvious annoyance is how the Go formatting standard — intrinsically a good idea — interacts with version control. Because aligned fields and block-scoped indentation are part of the spec, a single-field struct change causes unrelated whitespace diff across an entire file. GitHub's ?w=1 hides whitespace changes, but it is not the default view, and it prevents inline comments on the affected lines4.

Governance Friction

Go's maintainers are decisive, which is mostly a strength — but it can also be a source of frustration in open-source contribution. A well-known example is the Golint issue requesting a non-zero exit code. Community members articulated the problem, volunteered to write the patch, and demonstrated clear demand. Instead of engaging with the arguments, a core developer rejected the proposal; even the attempt to respond coherently was minimal. The issue was closed and the discussion locked, and the suggestions were discarded in sequence without a defensible rationale3.

That specific issue was later reopened by Russ Cox — presumably after public complaints — and eventually resolved. Still, the episode illustrates a pattern of community governance that can make external participation feel uninviting.

Verdict

The language has its flaws: missing generics and compiler-enforced formatting rules are real costs, and managing large codebases requires discipline to avoid repetitive bloat. For all that, though, Go is pleasant to work with, easy to learn, and fast for production development. It lacks the deep safety guarantees of stricter systems, but offers a pragmatic balance that makes it well suited for contemporary infrastructure. While it may never inspire the same passion as some other languages, Go is a thoroughly solid choice for the middle ground between control and convenience.