So you are a Java dev who wants to learn Go. Be welcome! And be prepared to enter a completely different world.
(Note: this is v2 of the article, based on the valuable feedback from the active and helpful Go community in
Go was created out of frustration with existing programming languages. Slow compilation speed, feature bloat—the mainstream languages of 2007 had become unwieldy and difficult to work with.
Interestingly, Go became quite popular among Pythonistas. They quickly learned that Go is much less complicated than other compiled languages like C++ or Java. And compared to Python, Go offers a much higher speed of execution and safer code due to strong, static typing.
What about developers coming from other languages? What about you, dear Java developer? You might not have as many incentives for moving over to Go as Pythonistas or C++ devs might have.
But here you are, on your way to moving from Java to Go, maybe because something in Go excites you, or you are planning to move to a space where Go is prevalent (think DevOps: Docker, Kubernetes, Traefik, Terraform, CockroachDB,... all built with Go), or (worst case), your current or future employer requires you to learn Go.
In any case, you are about to enter a vastly different ecosystem. The language, the toolchain, the philosophy—when you enter Go, you will find some things missing, some new things, and some things different from the Java world.
All of these differences are rooted in Go's philosophy.
(If you already decided to learn Go and are looking for a good Go course, have a look at Master Go)
You may have heard Java being called "the language of nouns". Indeed, Java focuses on constructing types first, then defining the interaction between types. "What objects do I need, and how do they talk to each other?" After all, Object-Oriented Design (OOD) is an attempt to model software like the real world, where subjects receive input and act on it.
Go takes a different route. Go starts with the verb, the doing. "What action needs to be done?" The primary building blocks in Go are functions. Yes, you can create types that have methods, but these do not need to be your starting point. Most packages of the Go standard library have top-level functions for immediate action.
This has a deep and wide impact on the design and implementation of libraries and executables. Most of the Gang Of Four (GoF) design patterns cannot be applied to Go unmodified or at all. Go also favors libraries as the standard building blocks of an application, rather than frameworks. (Frameworks do exist in Go, but none of them has made it into a "standard", and this remains very unlikely to happen.)
You can have a glance at Go's essential nature by reading the Go Proverbs (or this version, which I beefed up with AI-generated limericks).
Enough philosophy. I bet you now want to learn what's ahead of you. Let me start with the top differences.
Interfaces in Go can be a source of confusion for Java developers. In Java, interfaces are predeclared and can contain a large number of functions. (Although it is good practice in Java to keep interfaces reasonably small.)
In Go, an interface should be as small as possible. Interfaces with only one or two methods are not an exception but rather the rule. See also the Go proverb: "The bigger the interface, the weaker the abstraction".
And what's this thing about not being predeclared? This is indeed a fundamentally different way of interface usage. In a nutshell,
Go has pointers. This means you can take the address of any variable and pass that address around. Unlike C pointers, pointers in Go are safe, as they disallow any pointer arithmetic and are backed by the garbage collector.
Java has references, so the pointer concept is not entirely alien. But in Go, the concept is more explicit. By default, all function parameters are passed by value. If you create a
struct (think "very simple Java class with fields and methods but without inheritance") and pass this struct to a function, the struct gets copied. In Java, an object created from a class is always passed as a reference. If you want to pass a variable by reference in Go, you need to tell the compiler to do so.
Read more about pointers in Go here.
The visibility of functions, types, or fields is defined at the package level. There are no keywords like public or private. Instead, an identifier is public if its name starts with an uppercase letter. (This implies that identifier names must start with a Unicode character that has uppercase and lowercase variants. But that's just an aside.)
This is perhaps one of the biggest productivity boosts in Go besides automatic memory management and a simple but effective language: Go code compiles insanely fast. Rather than requiring minutes to hours of build time, typical Go projects compile in seconds or minutes. And we are talking about compiling to native machine code.
Some aspects of Go do not exist in Java at all, or at least not in the core language. Here are a few examples.
Concurrency in Go is built right into the language, no library needed. And the interface is simple as can be. The main ingredients are:
There is no management interface available for goroutines. No run, pause, stop, status, nothing. Goroutines are meant to be short-lived. Start one when a task is to be done, and have it finish its task as soon as possible.
Not only do Go binaries need no virtual machine to run, they even do not need any dynamic libraries pre-installed on the system. No "DLL hell". A Go binary is always statically linked. You can compile it on machine A and run it on machine B without worrying about missing runtimes or libraries.
Java also has an option for compiling native binaries, even statically linked ones, by using GraalVM, an alternate JVM that can do ahead-of-time compilation. But believe me, it's more fun if that feature is right built into the core toolchain and is as easy as calling
Pure Go code cross-compiles trivially. What does that mean? You can compile a Go binary for Linux on a Windows machine, without the need for installing a specific compiler toolchain for the target system. The same applied to different architectures and any combination of architecture and operating system. Does your build server run on Windows/i86 and your production servers on Linux/ARM? No problem.
And of course, the resulting binary needs no pre-installed libs or VM on the target system—see "self-contained binaries".
Functions in Go can be used as variables and passed to or returned from functions. Look at this example from net/http that creates two functions h1 and h2 and passes them to
http.HandleFunc to set up HTTP handlers for different endpoints. This is even possible for methods! Consider functions in Go like a generalization of Java's lambda expressions.
Some Java features are simply non-existent in Go with no direct replacement, like inheritance, operator overloading, or exception handling.
In Go, you can construct objects, but you cannot inherit behavior from "parent" objects. This is not as limiting as it may seem. Go has mechanisms like interfaces and struct embedding that help compose new functionality from existing functionality. And remember that Go is verb-oriented. A well-designed library filled with functions and (flat) objects is a decent starting point for code reuse.
Go has no method overloading, and intentionally so. The Go FAQ explains the reasons here. TL;DR: the lack of overloading makes code less confusing and fragile and also simplifies the type system.
Design decisions like this make Go code essentially non-magic. Go code clearly says what it does. What you see is what you get.
The Go designers deliberately omitted exceptions from the language. Error handling in Go is direct and explicit. This forces developers to think about how their code can fail and provide useful error messages and context for each and every error that can possibly happen in the current function. Readers of the code immediately see where code can fail, and what it does in that case.
Agreed, the resulting code is much more verbose, and critics say that sometimes, the error handling code obscures the "happy path" of a function. But this is because non-fatal errors are part of a function's logic. (Go has a separate mechanism for fatal errors.) If a statement can error out, this fact should be visible in the code, right where the statement is called.
This was a short tour-de-force over the essential differences between Java and Go. And I could go on and on, but hey, this is a blog article and not a book. I have to stop at some point.
But stopping here makes me feel a bit guilty because there is so much more to learn when switching from Java to Go. So I want to invite you to visit java2go.dev where you can learn more about Go from a Java developer's perspective.
And when I say "from a Java developer's perspective," I mean it. Preslav Rachev is a Java developer who worked his way into the Go language and knows the differences well—differences in philosophy, paradigms, idioms, and language constructs.
If you already decided to learn Go, you may want to take a good online course for gaining a profound knowledge of the language and the skills needed to succeed as a Go developer. Whether you plan to learn 9-5 or only in the evenings or during the lunch break, a self-paced online video course like Master Go can fit your needs.
Applied Go Courses helps you get up to speed with Go without friction. Our flagship product, Master Go, is an online course with concise and intuitive lectures and many practices and exercises. Rich graphic visualizations make complex topics easy to understand and remember. Lectures are short and numerous to save your precious time and serve as quick reference material after the course. Learn more at https://appliedgo.com.
Java Duke (c) New BSD-License
Background photo by Joshua Sortino on Unsplash
Categories: From X To Go, Java