My first steps in Go
From time to time, many of us software engineers change the programming languages we use. As you might know from our other articles, at Outreach, we primarily use Go for back-end development, and that’s how I began learning it. I’d like to share this life-changing experience that happened to me some time ago and hopefully inspire more people to switch to Go. We’ll introduce Go briefly, cover the basics and main principles, explore key characteristics and built-in features, discuss error handling and OOP, and delve into more advanced topics like generics and concurrency.
Getting to know Go
Go, sometimes called Golang (to differentiate it from the game), is one of the newest languages from a computer-era perspective, dating back to its public launch in 2009. It is slightly older than Kotlin (circa 2011), whose popularity has also grown in recent years. Before joining Outreach, I spent the last couple of years working with this JVM-oriented language, and that’s why I will refer to it in this post to make a comparison with Go.
I was first introduced to Go several years ago when I had to handle a hiring task. I remember opening the documentation and taking a tour, both of which were pretty straightforward. Since then, I’ve been working with Kotlin, but Go has continued to catch my attention.
Joining Outreach
Everyone knows that feeling when you have to find a new job. The hiring process usually consists of several steps, one of them is real-time programming or a specified task. You can find more about the interview process and how we do it at Outreach in our previous article.
I had poor knowledge of Go when I applied for a position at Outreach. Luckily, it was not a crucial requirement to know Go to pass the interview process, as the online programming exercise focused on problem-solving rather than deep language knowledge. For me, this round of pair programming was also quite challenging because I had just returned from a sabbatical and hadn’t developed anything for a couple of months. Just like with human languages, when you don’t use them, you start forgetting them. Although my professional career has already lasted more than a decade, during the interview tasks, I was unsure about some basic Kotlin syntax, which led to a few funny moments.
Finally, I joined Outreach and began getting to know not only the new environment, technologies, architecture, office, and amazing colleagues but also the new programming language. If I count the major programming languages I have dealt with so far, Go is already the sixth one.
Go basics
Starting with Go means going back to the fundamentals. It’s not only about native compiling instead of a virtual machine environment or just-in-time compilers (interpreters) but also about making code simpler with a focus on strict formatting and readability. “Keep it simple” is one of the main principles. However, there are no objects in Go; instead, we deal with structs and interfaces, composition inheritance, and methods attached to structures (called receiver methods), along with many other object-like features. This was the first challenge I had to face: learn how to code without object-oriented programming.
Once I refreshed my memory on what pointers and references are, and how to work just with scalar types, I could move on to learning the syntax formatting rules. Luckily, there’s an excellent built-in linter that checks the code very thoroughly and fixes some major formatting issues. Basic coding in Go is really simple, and you don’t have to write semicolons at the end of each statement, which, in my opinion, is a real relic of the past.
Working with variables
The Go Garbage Collector takes care of cleaning up memory, which makes many tasks easier. However, there are still many cases where it’s valuable to think about memory management. For basic memory usage, one has to learn several patterns. There are also advanced techniques that require deeper knowledge of how Go uses the stack and heap to manage memory.
We can use several scalar types (including strings), and it’s possible to choose the size of a number value, including unsigned types. Only scalar types can be defined as immutable constants. All other variables are mutable, and there’s no way to protect them from changes.
When not initialized, each scalar type has a default value. It’s common practice to compare strings with an empty value instead of using a pointer and checking it against nil.
I came from a world where nil/null is handled by the language syntax. It took me some time to adjust to this pattern. Generally speaking, if Kotlin is primarily developed to handle nullability through syntax (and avoid NullPointerException, which appears very often in Java), in Go, I’ve returned to the stage where I have to always check for nullability when using pointers. Last but not least, I need to think about when to pass variables as pointers and when to let Go make a copy of them for local usage.
Collections
When delving deeper into understanding Go, I had to get acquainted with slices and maps. A slice is essentially an array that supports built-in basic operations like adding elements, accessing members, deleting elements, and slicing. The same applies to the map type. In both cases, there’s no immutability or thread safety by default.
When I compare it to the extensive collection framework of Kotlin, there’s significant room for improvement. Fortunately, the slices package has been recently added to the standard library to partly fill this gap, and there are also third-party libraries available. We should still keep in mind that the code should be simple and readable, so sometimes it’s better to see a foreach loop in the code than to try to understand some built-in/external, obscure function performing internal magic.
Multiple returns
I haven’t dealt with many languages that provide multiple return values. It was always a bit painful when I had to create an object and name it just to return more values from one method. Python has this nice feature, and I’m glad that the creators of Go decided to incorporate it as well. I often find it helpful to name those return values to provide a better understanding of what the result stands for. This mechanism is also used for error handling.
Below, the example function has one parameter input of the type int and returns two result values: an answer of type int and err of the type error. The second value is used for error handling, which is elaborated on in the next paragraph.
func getMeSomething(input int) (answer int, err error) {
if input > 10 {
return 42, nil
}
return 0, errors.New(“invalid number”)
}
Error Handling
Coming from a world where exceptions can be thrown and handled anywhere, I had to adjust to checking for returned errors every time. The linter complains if you don’t do that. The error, if it can occur, is usually the last return value of the method, and you, as an engineer, have to handle the situation when an error occurs. The if err != nil statement is one of the most frequently used ones in the code. In most cases, you pass the error upstream (to the upper calling method) and have an error handler respond to the caller with the proper state. One of my first tasks was to go through the source code and improve the error handling in one of our services.
res, err := someMethodWithResultOrError()
if err != nil {
// deal with the error
}
OOP
As mentioned earlier, Go is not an OOP language. We can consider it more procedural; however, it’s still possible to view structures as autonomous objects with attributes and functions. From my perspective, it’s mostly the terminology that differs. It’s not a class but a structure; it’s not an instance/static method but a receiver method.
There are also interfaces available, allowing us to implement well-known design patterns (for example, my favorite one, Strategy). At first glance, it’s not obvious that a structure implements an interface. This might seem like a bit of magic, but I got used to it over time.
Typical object inheritance is replaced by a composition model, which is even more powerful. It essentially allows multiple inheritance, which is a very unique feature compared to other programming languages I know.
Generics
The Go language is becoming increasingly popular and continues to receive new features. I was pleased to discover that I can use generics. Some might disagree, but I see it as a powerful feature to avoid code duplication. Along with the composition pattern, generics can reduce a lot of boilerplate code, simplify many tasks, and prevent bugs. While refactoring a small part of the code, I was able to solve a previously overlooked bug thanks to generics.
Concurrency
As I became accustomed to the syntax and major features of Go, I began to learn more advanced ones. One of the most powerful and fundamental features is Goroutines, along with the synchronization toolchain for them, called channels. When considering concurrency, this combination of features provides a simpler and more efficient way to handle it compared to traditional threading.
Conclusion
There are many more useful features that I haven’t mentioned in this article, such as dependency management and package visibility. Check the Go documentation if you want to learn more. After a few weeks, I was able to work in Go without searching for the basics. After a few months, I could confidently handle Go as my primary programming language. However, it’s still sometimes better to consult the documentation or ask one of my great colleagues at Outreach.
I’d say that any engineer should not be afraid of changing their main programming language. It broadens your perspective on how to tackle tasks and solve various problems from different angles. I’m happy about this change as it provided me with the opportunity to learn something amazing. I hope to inspire more people to give Golang a chance or to apply for any of our open positions.