CREDIT: meineresterampe via Pixabay (CC/2.0)

There’s a bit of a love/hate thing between Golang and me. Using Go for greenfield development can feel dull and repetitive. The language is boilerplate-heavy, feature-light, and leaves little room for metaprogramming or syntactic cleverness.

But if Erlang has processes and Rust has a type system, Go’s killer feature is maintainability. On big projects, small projects, or projects actively developed by a small army of contributors, the same restrictions that can make Go feel like a chore make for software that’s unusually easy to maintain.

When you’re on the hook to deliver beyond the next sprint, maintenance matters. A lot. So, whether your day-to-day work is in JavaScript, Rust, or some other prevailing language-of-the-week, it’s worth learning (and replicating) many of the lessons Go has on offer.

If there’s only one thing to take home from Go, it’s how little sugar it takes to build great things. No templates, generics, or class-based inheritance? No problem. Not having them makes the language faster to learn and easier to use.

Walk up to a random Go program and you’ll encounter patterns that you’ve seen in 93% of other Go programs. Valid statistic or not, you get the idea: with only one or two ways to do something, there isn’t much room for cleverness. The details particular to a new package’s domain will still demand a quick glance through the package documentation but the idioms and structure of the core language tend to be familiar (if not self-evident).

Features don’t make the language. Beyond some modest threshold, preferring simple, easily-understood facilities over sugary abstractions will encourage software that’s easier to reason about, debug, and maintain.

Conventions serve several purposes. They’re a useful way to build cohesion, of course, but they’re also a quick end to many sometimes-heated (and often-meaningless) debates.

It’s no surprise, then, that Golang establishes a number of conventions to minimize configuration, simplify project structure, and save time and energy for important things. Some of the more notable ones include:

one $GOPATH to rule them all - All go packages are stored in the directories of a system-wide $GOPATH , regardless of how they got there. Forget the hours spent trying to understand how a specific dependency is resolved: in Go, there’s just one way to do it.

directories are packages - every source directory within the $GOPATH follows a similar structure. What’s more, each directory is treated as an independent package. References to code in other directories requires an explicit import , which–besides enjoying nice symmetry with the filesystem–encourages encapsulation and simplifies dependency resolution.

tests are next door - As both a side-effect and benefit of the directory-as-package structure inside of $GOPATH , each module’s test definitions live right beside it. There’s no need to spelunk around the directory tree in search of a lost test directory: verify.go and verify_test.go are right beside each other. It doesn’t get more obvious than that.

code formatting - In Go, formatting is defined within the core toolchain. Yes. No more debates about whitespace or semicolons: just run go fmt (if the IDE hasn’t) and never worry about indentation again.

visibility - Capitalized definitions are exported; everything else is private.

Even from a few samples, the goal is clear enough: dependable rules remove unnecessary cognitive load. Style is established, dependencies are clear, and many needless debates over organization are pre-empted.

Many of the benefits that Go offers (human) readers also make life easier for machines. Simpler syntax and predictable project structure lead to simpler parsing, faster builds, and performant tooling. That means faster feedback and happier developers, through:

Fast static analysis

Automatic documentation generation

High-quality IDE integrations

Even as the codebase grows, the relative independence of Go packages make it possible to focus change (and therefore verification) on distinct, isolated corners of the codebase. While build- and test-times increase linearly with the volume of code, cleanly partitioned packages make it possible to retain fast feedback even in quite sizable projects.

Go’s standard library shadows the design of UNIX’s own, albeit with four decades more experience with interface design and fewer assumptions about the underlying operating system.

The standard library is far from exhaustive, but for network programming you generally get what you need. Better yet, its APIs are specified in terms of a small set of simple, consistent cross-platform interfaces. Developers familiar with io.Reader are well on their way to understanding how to decrypt cipher text or read an HTTP request; familiarity with Go error-handling will cover the failure states of nearly every core method.

The standard library doesn’t preclude higher-level abstractions, of course. Go has a vibrant ecosystem of third party packages that adapt existing functionality and implement proprietary APIs. But working directly through the standard library is often as expedient (and nearly always more direct) than working through a userland wrapper.

All of this is great news for productivity. Not only does a strong standard API reduce the number of new libraries for developers to learn, but time that would otherwise be spent looking for alternatives and managing dependencies can now be applied to the specifics of the problem at hand.

I’m not in love with Go; at least, not without the fair share of pricks and barbs that follow from any well-worn relationship. Other languages (pick one) offer features, syntax, and freedom for their users to shape their domain as they please. Go’s simplified, C-style syntax is objectively un-featured and subjectively un-“fun”. If code is poetry, Go is its McGonagall.

Still, no matter the allure of a fetter-free language and a shiny green field, software development is a business. When it’s time to get things done, Go has plenty of lessons to teach.