I had written maybe 500 lines of Go before the new role. Within two months it was my primary language. This is the honest transition account — not a “Go vs Java” opinion piece, but what the practical experience of switching felt like.

What’s Immediately Comfortable

The toolchain is a relief. go build, go test, go fmt — everything works, they’re consistent, and go fmt means no formatting arguments on code review. After years of IntelliJ reformatting battles and Checkstyle config archaeology, the opinionated formatter felt like someone had removed a low-grade background irritant.

The compilation speed. Java with a large classpath compiles slowly. Go compiles fast — the kind of fast where the feedback loop feels different in a qualitative way.

Error handling, once you accept the idiom. After the initial “why isn’t this a checked exception” phase, the explicit error return pattern becomes readable. The call chain is visible in the code, not hidden in a throws declaration that might or might not be accurate.

What Requires Unlearning

No inheritance. Go has embedding and interfaces, but no classical inheritance hierarchy. Spending seven years with the OOP mental model means your first instinct for most problems is “what’s the class hierarchy?” In Go the right question is “what are the interfaces?” — and usually the interface should have one or two methods, not fifteen.

This forced a genuine design rethink. Small interfaces compose better. io.Reader and io.Writer are tiny; the ecosystem that’s built on them is enormous.

Concurrency model. Java’s concurrency is threads + locks (or futures + executors). Go’s is goroutines + channels + the sync package. The goroutine model is cheaper than threads (2KB stack vs ~512KB default Java thread stack), which means you can have hundreds of thousands of them. The mental model for structuring concurrent programs changes when the unit of concurrency is that cheap.

I initially tried to write Java concurrent patterns in Go — mutex-guarded shared state everywhere. It worked but felt wrong. The idiomatic Go approach (for the problems I was solving) was communicating via channels rather than sharing state. Took about a month to internalise when to use channels vs when a mutex is actually the right tool.

Interfaces are implicit. In Java, you declare implements SomeInterface. In Go, if your type has the methods, it satisfies the interface — no declaration needed. This is duck typing with static verification. It’s powerful (you can retroactively make a type satisfy an interface defined in a different package, without modifying either) but requires discipline: without explicit implements, it’s easy to accidentally satisfy an interface you didn’t intend to, or to not realise you’re failing to satisfy one.

The Things Go Just Doesn’t Have

  • Generics (pre-1.18): the workaround was either interface{} + type assertions, or code generation. Both are worse than generics. We used code generation for collection-type utilities and accepted the verbosity.
  • Exceptions: Go’s panic/recover is not idiomatic for normal error handling. The pattern is return values. Some things that would be one-liners in Java with exception handling are four lines in Go with error checks. I stopped minding this more quickly than I expected.
  • A rich standard library for everything: Go’s stdlib is good for networking and I/O, adequate for most things, and absent for some things Java has had for twenty years. Third-party library quality is variable.

The Performance Characteristics

Go’s GC is lower-latency than Java’s (in the sub-millisecond range for modern versions, with short but frequent pauses). It’s a trade-off: lower tail latency, but throughput is lower than Java with a well-tuned G1 or ZGC. For the API and data pipeline work I was doing, Go’s characteristics were better suited than Java’s — I cared about tail latency for API responses, not throughput.

The lack of JIT means Go’s performance profile is more predictable but you lose some of the adaptive optimisations Java’s JIT provides. For hot loops, Go is often slower than Java with a warmed-up C2 JIT. For server workloads with diverse call patterns, the difference is usually smaller than the benchmarks suggest.

Three Months In

By month three I was writing idiomatic Go without conscious effort. The idioms are fewer and more consistent than Java’s, which helps. The things I missed from Java: the IDE support (still better in IntelliJ for Java), the reflection-based tooling (some libraries I’d relied on in Java don’t exist or are weaker in Go), generics.

The things I didn’t miss: the verbosity of the type system for simple cases, the dependency management chaos (Maven/Gradle vs go modgo mod wins), the startup time.

The things that surprised me most: how much I valued go vet and staticcheck as correctness tools once I started using them consistently, and how much better the concurrency primitives felt once I stopped trying to write Java patterns in Go.