
Introduction
More than three years ago I joined Meroxa to work on Conduit and its connectors. This job change brought with itself another change for me: the programming language. Conduit and almost every of its connectors are written in Go. In the 11 years before that, I was working with Java. It’s been a change in many ways, I must confess.🙂
That’s also a change many more developers are either making or thinking of making, so I’d like to share my experience hoping it will make your journey to Go better.
This blog post is the first I plan to write on this topic. I'll share how I learned Go, give a brief overview of the language, and then dive into the differences between Java and Go, specifically in terms of interfaces and functions.
How I learned Go
A great way to start with Go is A Tour of Go. It’s a tutorial made by the Go team that introduces you to the basic concepts in a very light way, but also lets you try out the code online!
However, nothing beats a good book when you want to learn systematically. The first book I was reading about Go was Go in Action, and then I switched to The Go Programming Language. Go in Action is a good book, but nevertheless, I like The Go Programming Language more, mostly due to the approach. Go in Action starts with a comprehensive example project that’s sometimes difficult to follow because a few concepts are shown at a time. The Go Programming Language on the other hand focuses on individual syntax elements and concepts and gradually builds the knowledge.
As for the hands-on experience, my first task was the Kafka connector. After that one was completed, I switched to some other connectors, Conduit itself, and I was also working for some time on what is now the Conduit Platform.
A very important method of my learning were code reviews. My colleagues’ patience when reviewing my code, and answering questions about the project(s) and Go helped me tremendously. Another thing that helped me was reviewing the code myself. It was rarely for the sake of trying to improve someone else’s code, but most often for the sake of seeing more code and asking questions about it.
A quick introduction to Go
Many call it Golang, but, officially, the language’s name is Go.
Go is a compiled language and needs no runtime in the sense Java does. When you find the Go runtime mentioned, what is meant by that is not a separate application that is executing a binary and managing it (like the Java Runtime does). What is meant by Go runtime are Go’s internal packages that get included in a build and that, for example, execute goroutines (Go’s green threads). In other words, users don’t need to have a Go runtime installed to be able to run applications written in Go.
The Go language is a lot about simplicity, clarity, and decoupling. Java code is sometimes known for its verbosity, and that’s true to an extent. Related to that is a famous Go proverb that says “A little copying is better than a little dependency.” It’s quite normal to grab an Apache Commons library to do a small thing or two in a Java project. However, in Go, you’ll just copy a few lines of code.
When you install Go you also get quite some tools, such as:
go build
for buildinggo test
for running tests (it works, but it's a shock after JUnit)go get
for getting dependencies,go install
for installing runnable tools (think of a package manager kind of thing)
Go itself provides a way to manage dependencies, which is go.mod file (similar to build.gradle
or pom.xml
). Dependencies are most often found on GitHub and fetched through pkg.go.dev (indirectly).
Now let’s get to the code!
Interfaces
Go interfaces and Java interfaces are declared in similar ways, so we won’t spend too much time on that. One difference is that Go doesn’t allow default interface methods, whereas Java does. The second, and probably biggest, difference is how the interfaces are used.
Java uses a nominative type system. Classes that implement an interface must use the implements keyword, i.e. they show which behavior they implement explicitly.
Go’s type system is structural. Structs implement an interface implicitly and the Go compiler checks if a value conforms to an interface. Here’s an example:
package main
import (
"fmt"
)
type FileReader struct {
}
func (f *FileReader) Read() string {
return "a line from a file"
}
type Reader interface {
Read() string
}
func main() {
// declare a variable of the type Reader
var r Reader
// Assign a pointer to a FileReader to r
r = &FileReader{}
fmt.Println(r.Read())
}
If you want to be sure that a struct implements an interface, you can add the following line:
var r Reader = (*FileReader)(nil)
A good practice is to define the interface where it’s used, and not where it’s implemented.
This unlocks some useful things. One is that you can always declare an interface, and make sure your code’s intention is clear: it needs the Read
method, not FileReader
itself.
Another benefit is visible in tests: since you can always define an interface yourself, you can also easily create mocks or write stubs for it and write better tests.
Functions
Functions in Go are generally what methods are in Java. Go has methods too, and those are functions defined on types, i.e. functions that have a receiver. It’s very close to object methods in Java, but there’s a difference. that we’ll explain in a later blog post. Here are the most important differences between Go functions and Java methods.
No overloading
In Go, no two functions in the same package can have the same name, regardless of the number of parameters or their types. At first, it might be a thing, but over time you get used to it, and just “work around it”, either by using generics, or by thinking if that’s really needed and if it actually needs to be simplified.
Multiple return values
A function can return values. You might be thinking: “No way, that’s horrible!” You have my full understanding, that was my reaction too. It immediately reminded me of the out
parameter in C#, yuck. However, it makes it possible to write simpler code for use cases that need to return 2 values, like splitting a string into a key and value, or getting the host and path from a URL, and so on. But, when you need to return 3, 4, or more variables, then it’s likely a code smell and usually it can be simplified.
Most often though, you’ll see a function return 2 values at most, and when that happens that’s usually a “real” return value and an error, like here:
// Sqrt return the square root of the input parameter.
// It returns an error if the parameter is negative.
func Sqrt(x float64) (float64, error)
We’ll explain the error
next.
Error handling
Go code signals error by returning error
values. error
is a special built-in interface:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
A nil error
denotes success; a non-nil error
denotes failure. In a way, it’s the Go version of Java’s Exception
.
In Go, you can construct your own error types (like in this example from the Tour of Go). But (and that’s another difference in thinking), most of the time you work with error values: you create new error values and check for error values. In Java code it’s not uncommon to see new exception classes being written, andtry-catch
blocks normally check for exception classes too.
Here’s some typical Go code that returns a new error value:
if x < 0 {
return 0, errors.New("cannot use negative number")
}
Or, you create an errors from existing errors, indicating the cause:
// ErrAlreadyRunning is an error variable.
var ErrAlreadyRunning = errors.New("pipeline already running")
func runPipeline(id string) error {
if isRunning(id) {
// we return a new error indicating that it was caused by ErrAlreadyRunning
return fmt.Errorf("couldn't run pipeline: %w", ErrAlreadyRunning)
}
// rest of code
}
It’s similar to creating a new exception in Java with another exception as a cause and a custom message. If our error handling needs to check whether an error has a certain cause, we again check for the value, for example:
err := runPipeline("abc123")
if errors.Is(err, ErrAlreadyRunning) {
fmt.Println("Pipeline is already running")
}
Wrapping Up
Transitioning from Java to Go is a journey of unlearning, relearning, and embracing simplicity. While at first, some concepts in Go—like multiple return values or implicit interfaces—may feel unconventional, over time they reveal their elegance and strength.
If you’re considering learning Go or making the switch, remember: it’s less about mastering a new syntax and more about adapting to a different way of thinking. And once you do, it’s a pretty rewarding shift. 🚀
I hope this post gave you a helpful starting point and some clarity on the key differences between Java and Go. In the following posts, I'll cover more topics like the ecosystems, packages (you’ll be surprised about this), structs, types of receivers, and more.
Want to stay in the loop? Join our Discord, follow us on Twitter, LinkedIn, or subscribe to our RSS feed for future posts!
Thanks for reading, and happy coding! 🧑💻✨