Thinking in Go: The Bad Habits You Lose When You Switch

Why Go keeps gaining ground, and what actually changes when you switch: not new techniques to learn, but habits from other languages you finally let go.

By Omar Flores
Table of Contents

The first Go code I ever wrote was Java wearing Go syntax.

I’m not proud of it, but it’s the honest truth. I came into Go after years of a class-heavy, framework-heavy world, and my first repository looked like it: a Service interface for every struct, a NewXService factory for every service, a builder pattern for a config object that had three fields, and a package called utils because some habits don’t even notice they’re being packed for the trip. It compiled. It worked. And every senior Go developer who opened a pull request from me could tell, in about four seconds, that the person who wrote it had never thought in Go.

The comment that started my real education was short. Not a lecture, not a style guide. Just: “This is correct Java. This is not Go.”

That sentence annoyed me for a week and then rearranged my career. Because it pointed at something most “learn Go” material never says out loud: Go is an easy language to write and a different language to think in. The syntax takes a weekend. The chip takes months. And the reason the chip has to change is that Go doesn’t work by adding techniques to your toolkit — it works by deleting habits you’d been carrying for years, some of which you were only keeping out of habit itself.

So this article is two arguments braided together. First: why a language this unglamorous keeps gaining strength, year after year, in an industry addicted to novelty. Second: what actually changes in your head when you switch — and which habits from other languages you can finally put down.

The momentum isn’t hype. It’s gravity.

Let’s get the numbers out of the way, because the pattern is strange and worth staring at.

On the composite indexes, Go sits around eighth to tenth among programming languages — never a headline act, never trending on the front pages. And yet, of the twenty projects that define the cloud native stack, nineteen are majority Go. Docker. Kubernetes. Terraform. Vault. Prometheus. etcd. Helm. The layer of the industry that schedules, coordinates, configures, and observes everything else runs on Go, to a degree that stops being a coincidence once you measure it.

Why does this keep compounding instead of fading the way “language of the year” stories fade?

The first reason is structural. Docker chose Go in 2013, and Kubernetes was rewritten into Go before its 2014 launch. That decision created gravity: the client libraries, the controllers, the operators, the extension points — the plumbing everyone needs to touch — came out Go-shaped. At a certain point, starting an infrastructure project in anything else meant re-implementing half an ecosystem before writing your first feature. Gravity is not a marketing effect. Once it exists, it’s a technical fact.

The second reason is that the properties making Go good for those tools are exactly the properties ops teams keep asking for. A Go program compiles to a single binary with the runtime linked in — no interpreter version to manage, no dependency tree to ship, CGO_ENABLED=0 and the container image is the program. Cross-compilation is one command from one laptop. Goroutines make “one lightweight task per connection” the natural design instead of a callback pyramid. And the compile loop is fast enough that a huge codebase rebuilds in seconds, which quietly changes how much a team is willing to refactor.

The third reason is the quietest one, and I think it’s the most telling. The Go team runs a developer survey every year, and satisfaction has held above 90% since they started asking in 2019 — 91% in the 2025 survey, with roughly two-thirds answering “very satisfied.” Stable satisfaction is the metric almost nothing in this industry achieves. Frameworks spike and crash. Languages age into resentment. Go’s number just sits there, year after year, which suggests people aren’t staying because of novelty. They’re staying because the day-to-day keeps being fine.

And the honest part of the ledger: Go didn’t win everything, and it keeps not winning things. The hot data paths — nginx, Redis, Envoy — stayed C and C++, and the newest ones keep going to Rust. If your problem is bytes on the wire at nanosecond budgets, a garbage collector is a real cost, and the industry knows it. Go’s position isn’t “best language.” It’s something more durable: the default answer for the control plane, for backend services, for the tools your team has to maintain for five years. A language that owns a tier of the industry without needing to win the front page has more momentum than its ranking suggests.

That’s the outside view. Now the inside view — what it costs you to actually get in.

The chip you have to change

Every popular language trains a reflex. Java trains you to think in class hierarchies. Python trains you to reach for the elegant one-liner. JavaScript trains you to compose frameworks until the framework is the product. These reflexes aren’t flaws — they’re optimized responses to what those languages reward. The disorienting part of Go is that it rewards different things, so all those trained responses misfire at once.

The first and biggest change: Go doesn’t do inheritance. Not “discourages” — there is no inheritance, and after a few weeks you stop missing it, because what Go offers instead is composition through small interfaces. You stop asking “what should this class extend?” and start asking “what is the smallest set of behaviors my caller actually needs?”

type Notifier interface {
	Notify(ctx context.Context, msg string) error
}

Notice what that interface doesn’t do. It doesn’t establish an is-a relationship. It doesn’t carry state. It doesn’t come with a base class full of hooks you must not break. It describes one behavior, and — this is the part that breaks people’s brains the first month — the types that satisfy it don’t have to declare that they do. Any struct with a matching Notify method just is a Notifier. Nobody signs a contract; the compiler verifies it structurally, at the use site, not at the definition site.

That single inversion dissolves half the design problems you learned to manage with design patterns. The Gang of Four book is, largely, a manual for working around the costs of inheritance-heavy languages. In Go, a suspicious number of those patterns reduce to “define a small interface and pass a struct.”

The second change: errors are values, not exceptional weather.

user, err := store.GetUser(ctx, id)
if err != nil {
	return fmt.Errorf("get user %d: %w", id, err)
}

Coming from a language with exceptions, this looks like punishment. I thought so too — the famous verdict is that Go error handling is “verbose,” and it is. But the verboseness is the point, and I can defend that with experience instead of ideology: in exception-based code, the failure path is invisible. It lives in whatever the runtime unwinds through, and the person reading the function has to trust that somewhere, in some frame above, someone thought about what happens when GetUser fails. In Go, the failure path is on the page. Every single time. You cannot write a call whose failure you’re silently ignoring without writing the word _ — and a linter will flag even that.

The error is a value, so you can wrap it, inspect it, branch on it, accumulate it, and the caller decides what failure means. Control flow and failure handling live in the same dimension. It took me months to stop fighting this and about a year to realize I no longer feared refactoring, because nothing could throw from four layers below and detonate in a catch I forgot existed.

The third change is a mindset shift more than a feature: the reader is the customer, not the compiler and not the writer. Go’s design decisions all point the same direction once you see it. gofmt makes every codebase look like the same person wrote it — your clever formatting dies at save time, and nobody mourns it for long. Dead code is a compile error in some cases, a vet warning in others. Unused imports break the build. The language actively refuses to let you be subtle. It sounds paternalistic for the first month. Then you review a stranger’s Go code and understand it at reading speed, and the paternalism starts feeling like a gift.

The habits you lose, one by one

Here’s the part I wish someone had told me earlier: most of the pain of switching isn’t learning Go’s features. It’s the moment each of your old habits stops compiling, or worse — compiles and gets rejected in review. Let me name them, because seeing them written down is oddly comforting.

DRY, taken as a religion. In most languages, duplication is sin and abstraction is virtue, so you inherit, generalize, and parameterize until three call sites share one function with four flags. Go culture holds that a little duplication is cheaper than the wrong abstraction — a line you’ve probably read before, and probably didn’t believe until you inherited the wrong abstraction six months later and had to unwed three call sites from it. Write the thing twice. If the third time reveals the actual shape, then extract. Duplication is cheap to delete. The wrong abstraction is expensive to divorce.

Exceptions as control flow. Beyond the readability argument, there’s a design cost I only saw once I lost the tool: when exceptions exist, every function has two exits and only one of them is written down. Interfaces in exception-based languages document the happy path and leave the failure path to folklore. Go’s if err != nil drags every possible failure into the open. You lose the brevity. You gain the truth.

Deep hierarchies and layers for their own sake. Controller-service-repository-dto-mapper, five files to move one field. Some of that layering is earned discipline; a lot of it is cargo cult from enterprise Java, a tax paid against problems your current service doesn’t have. Go codebases tend to be flat — a package per concern, structs passed directly, no DTO kingdom unless serialization genuinely demands it. The first flat codebase you work in feels naked. The second one feels fast. The third one makes you angry at the old ones.

Reaching for a framework before reading the standard library. In most ecosystems, the standard library is a floor you build frameworks on top of. In Go, the standard library is most of the building. net/http is a production-grade server. encoding/json, database/sql, crypto, testing — the survey data behind Go’s satisfaction numbers is mostly people saying the same thing: the platform under the language is the product. I have watched teams spend a week wiring a web framework around problems net/http and one middleware function would have solved in an afternoon. The habit to lose is assuming you need more than you do.

Cleverness. Operator overloading, metaprogramming, DSLs inside the language, one-liners that make reviewers feel stupid. Go declines all of it. Every one of those features is a way for the writer to be impressive at the reader’s expense, and Go takes the writer’s side of the bargain away. This is the habit that hurts the most to lose and the one that pays the most once it’s gone — because cleverness is a loan against the maintainer, and you eventually become the maintainer.

Notice what all five have in common. None of them is a Go feature. They’re habits other languages rewarded, and Go simply stops paying out. That’s why switching feels less like learning and more like withdrawal.

Idiomatic Go, in the wild

“Idiomatic” sounds mystical until you see that it’s mostly a short list of repeatable decisions. These are the ones that changed how I write everything, not just Go.

Accept interfaces, return concrete types. If a function takes a Notifier, its callers can pass anything with that method — including a test double. If it returns a concrete *EmailNotifier, callers get full access to the real type. Interfaces at the parameter, struct at the return: callers stay flexible, implementations stay honest.

func NewOrderService(store Store, notify Notifier) *OrderService {
	return &OrderService{store: store, notify: notify}
}

Define interfaces where they’re consumed, not where they’re implemented. This is the inverse of almost every language you’ve used, and it’s the habit that took me longest. In Java, the interface lives next to the implementation and everyone implements it. In Go, the consumer declares the smallest interface it needs, and implementations satisfy it accidentally. The result is interfaces with one or two methods — Reader, Writer, Notifier — and dependencies that are impossible to over-couple.

Lean on zero values. A struct in Go is never uninitialized. var mu sync.Mutex is ready to use. An empty slice is a valid slice. Once you internalize this, entire categories of constructor boilerplate and init ceremonies from other languages simply stop being written.

Pass context.Context as the first parameter of anything that does I/O or can block. Cancellation, deadlines, and request-scoped values travel with the call chain. The first time a deploy times out cleanly because a context deadline propagated through six layers instead of hanging a server for ten minutes, this stops being convention and starts being architecture.

None of these is deep. That’s the point. Idiomatic Go isn’t a body of secret knowledge — it’s a handful of consistent decisions, applied without exception, until the codebase reads the same way at every level. The difficulty was never intellectual. It was the discipline of doing the boring thing every single time.

What it feels like six months in

Let me close the loop with the honest ledger, because the pitch shouldn’t sound free.

The verbosity is real. You will write if err != nil thousands of times, and some days it feels like paperwork. Go’s expressiveness is genuinely lower than Python’s or TypeScript’s for the same logic; you’ll sometimes type twelve lines what another language does in five. The ecosystem is smaller than npm or PyPI in raw package count, though I’d argue the quality-per-package ratio and the standard library close most of that gap. And the domains where Go is simply the wrong tool haven’t moved: heavy data science is Python, browser is JavaScript, hard real-time and hot-path systems work belongs to C and Rust. The 2025 survey still shows most Go developers building CLIs and API services — that’s the language’s center of gravity, and pretending otherwise sets people up for disappointment.

But here’s what the other side of the ledger looks like, from someone who made the switch and never switched back. Onboarding a new developer into a Go codebase takes days, not weeks, because the code looks the same everywhere and there’s one way to format, one way to handle errors, one obvious place to look. Code reviews get shorter, because half the arguments that used to happen are pre-decided by the language. Refactoring stops being terrifying, because failures are explicit and the compiler is opinionated about leftovers. And services you wrote years ago still build, still deploy as a single binary, and still make sense when you open them cold.

The momentum numbers — the nineteen-of-twenty control plane, the stable 91% satisfaction — aren’t really about Go at all. They’re about what happens when a language optimizes for the person maintaining the code at 2 a.m. instead of the person impressing the code review at 2 p.m. That optimization compounds slowly, invisibly, the way good habits do. It’s just that you have to change your own habits first to feel it.

Go won’t teach you much. What it will do is make every habit you brought with you visible enough to finally put down.

mindmap
  root((Thinking in Go))
    What you lose
      DRY taken as religion
      Exceptions as control flow
      Deep class hierarchies
      Framework-first thinking
      Cleverness
    What you adopt
      Small interfaces, consumed where used
      Errors as values
      Zero values
      context everywhere
      Standard library first
    Why the momentum compounds
      Docker and Kubernetes gravity
      Single binary deploys
      Goroutines for daemons
      Fast compile loop
      91% stable satisfaction