Introduction
Not so long ago, I was given an opportunity to take a completely new step in my programming career. Indeed, this was one of the biggest and most satisfying challenges I have ever had as a developer. Almost five years have passed since I took my first steps in the IT world, but recently, I have felt like a rookie again, starting something completely new.
Without further ado, I was assigned to a new project, which had to be written in a language I had only heard of at the time – the Golang. Initially, I felt slightly overwhelmed by it because it is pretty specific compared to Java. But day after day, I got to know it better and felt more confident using it.
That is why I have decided to write the following article and share my thoughts about Go.
Yes, I am fully aware that I am at least five years late in bringing something new and sensational about this language. Probably everything that could have been written about Go was written already. Why do I care, then? Well, I want to present my point of view and show what it looks like to step into Go from a Java Developer perspective. Maybe some of you have had a similar experience or are considering choosing Golang for a project.
I will point out some of its features and compare them to Java. What will be distinctive about it is my personal bias. I don’t want to create another technical documentation. There are dozens of them within a grasp of the hand. Of course, I will discuss some technical aspects and present several examples, yet I will enhance them with my thoughts and observations.
How to classify Golang?
What Go actually has to offer? As a relatively new language, it is a mixture of concepts and ideas, well-known from other already-established programming languages. Some resemble their precursors (like pointers), while others are redesigned in a completely new way (for instance, error handling).
As a Java person, the greatest difficulty is describing what Go is all about. In the case of Java, it’s extremely easy to answer this one. It is an object-oriented language, which lately tries to be functional.
Is Go object-oriented? According to the answer from Official Golang’s FAQ – Yes and no. If you’re interested, you can read the whole answer here: Is Go an object-oriented language
There is no concept of class or inheritance. But, Go allows an OOP and makes the big advantage of simplifying this concept. Basically, if two types implement all methods defined by an interface, they can be used interchangeably wherever the type is defined as an interface. You don’t even have to explicitly specify which interface is implemented. Your struct just implements it, when the criteria are met.
Moreover, there is no need to specify any method in such an interface, which is very convenient. This is a clean and lightweight approach to object-oriented principles. It provides a simple yet flexible solution without the burden of more traditional inheritance, which can sometimes be complex and cumbersome to maintain.
Below there is a code snippet with an example of the Vehicle interface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | package main import "fmt" func main() { car := Car{ engineCapacity: 1.2, hasAc: true, } bike := Bike{ engineCapacity: 0.35, colour: "red", } apple := Apple{ weight: 0.18, price: 0.39, } vehicles := []Vehicle{car, bike, apple} for _, vehicle := range vehicles { fmt.Println(vehicle) } } type Vehicle interface{} type Car struct { engineCapacity float64 hasAc bool } type Bike struct { engineCapacity float64 colour string } type Apple struct { weight float64 price float64 } |
When running a program, you will get the following response:
1 2 3 | {1.2 true} {0.35 red} {0.18 0.39} |
As you can see, Golang is so flexible that even an apple became a vehicle, so you had better be careful with empty interfaces because all structures implement it implicitly! The best idea is to add and implement methods to an interface, but there is no guarantee that no other interface describes the same methods.
Let us upgrade our example as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | for _, vehicle := range vehicles { fmt.Println(vehicle) vehicle.startEngine() } } type Vehicle interface { startEngine() } func (c Car) startEngine() { fmt.Printf("Car engine(%vL) running\n", c.engineCapacity) fmt.Printf("Is AC running? %v\n", c.hasAc) } func (b Bike) startEngine() { fmt.Printf("Bike engine(%vL) running\n", b.engineCapacity) } |
If we add the startEngine() method to the interface and implement them for Car and Bike types, the Apple type no longer implements our interface. When running the code, the following error appears:
1 2 3 4 | ./prog.go:18:35: cannot use apple (variable of type Apple) as type Vehicle in array or slice literal: Apple does not implement Vehicle (missing startEngine method) Go build failed. |
The Go compiler provides us with very specific information about which variable is wrong and which method is missing. After removing apple from our list, the following output is obtained:
1 2 3 4 5 | {1.2 true} Car engine(1.2L) running Is AC running? true {0.35 red} Bike engine(0.35L) running |
As you can see in our implemented methods, we can access fields specific to our structs. That’s great, but how to do it when we do not know what type of object we have? Golang will not automatically do type conversion for us, so how can we deal with such a situation?
Fortunately, Go provides a couple of mechanisms: Type Assertion and Type Switch.
Below, there is a modified code snippet with an exemplary implementation of the Type switch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | vehicles := []Vehicle{car, bike, nil} for _, vehicle := range vehicles { doSwitch(vehicle) } } func doSwitch(i interface{}) { switch v := i.(type) { case Car: fmt.Printf("Is AC running? %v\n", i.(Car).hasAc) case Bike: fmt.Printf("Bike has colour: %s\n", i.(Bike).colour) default: fmt.Printf("I don't know about type %T!\n", v) } } |
The program outputs the following:
1 2 3 | Is AC running? true Bike has colour: red I don't know about type <nil>! |
Java offers a similar feature as a Preview starting from JDK17. As shown above, Golang promotes a fresh approach to object-oriented programming and provides simple yet efficient mechanisms to complete the work. But it is not strictly OO language.
What is it, then? Maybe a functional programming language? Not really, but it does have some well-known mechanisms that make functional programming effortless and pleasant. First of all: Golang functions are treated as values. They can be assigned as variables, stored in a map or slice, passed as parameters to other functions, and even returned from functions. Moreover, Golang provides closure functionality, which allows an inner function to access and modify variables declared in the outer function.
These features might be useful for data manipulation, such as sorting or filtering slices, where necessary implementation can be passed as a parameter. Another application example is using closure as a handler or callback function. Such a solution is used in the Golang HTTP package, where endpoint handlers can be wrapped in functions that can provide additional (middleware) functionalities, i.e., authentication. Below is a simple example that presents how an HTTP request can be intercepted to check credentials from request headers. It is a very naive and unsafe way of authenticating a user, which should not be used in real applications.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | package main import "net/http" func main() { http.HandleFunc("/login", login) http.ListenAndServe(":3000", nil) } func login(w http.ResponseWriter, r *http.Request) { user := r.Header.Get("user") if user != "username" { w.WriteHeader(401) } else { w.WriteHeader(200) w.Write([]byte("Hello user")) } } |
Except for the closure example, it is worth noting how easy it is to write a web service in Go. Thanks to the net/HTTP package, which has a ready-made HTTP server, it takes 10 lines of code to spin up a web app. How does Java compare to Go in terms of functional programming?
The answer might be slightly surprising, but not so bad since it introduces lambda expressions. Let us compare both with examples of filtering even and odd numbers from arrays. Golang’s implementation goes first:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package main import "fmt" var even = func(num int) bool { return num%2 == 0 } var odd = func(num int) bool { return num%2 == 1 } func main() { numbers := []int{1, 2, 3, 4, 5, 6, 7, 8} fmt.Println(filter(numbers, even)) fmt.Println(filter(numbers, odd)) } func filter(arr []int, filter func(num int) bool) []int { filtered := []int{} for _, num := range arr { if filter(num) { filtered = append(filtered, num) } } return filtered } |
As you can see, there is a function called filter, which accepts an array and a function. Basically, all we do is iterate over an array and call an inner function, which checks if the filtering condition is met. We can pass any function if its signature matches the one described as a parameter. Let’s move on to the same implementation done in Java.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package functions; import java.util.ArrayList; import java.util.List; public class Main { private static IntFilter even = num -> num % 2 == 0; private static IntFilter odd = num -> num % 2 == 1; public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8); System.out.println(filter(numbers, even)); System.out.println(filter(numbers, odd)); } public static List<Integer> filter(List<Integer> arr, IntFilter filter) { List<Integer> filtered = new ArrayList<>(); for (int num : arr) { if (filter.doFilter(num)) { filtered.add(num); } } return filtered; } } @FunctionalInterface interface IntFilter { boolean doFilter(int num); } |
Well, I must admit that it does not look bad. One significant difference is the need to declare and use functional interfaces. Such an interface has to have one method signature. The good practice is to put the @FunctionalInterface annotation. Having that, Java reveals many features of functional programming in front of us. Functional Interface can be returned from the method, as well as a method parameter, variable or static constant. Moreover, we can reduce some of the code by using default Java functional interfaces if they satisfy our needs. For example, IntFilter could be replaced with IntPredicate.
Which approach is better, in my opinion? I will opt for Golang because it is a little bit cleaner solution, easier to understand due to more traditional syntax, and does not require additional code such as functional interface declaration. I do, however, reckon that Java did a great job with lambda expressions. It’s very flexible and has a neat arrow syntax.
So far, we have spoken about object-oriented and functional aspects of Golang, but so far, did not manage to categorize it. We can say that Golang is an imperative and, more specifically, procedural programming language like most programming languages are. And, to be honest, this piece of information is utterly useless. In fact, trying to classify it to a specific type is against the idea of Go and may lead to producing an unidiomatic code. I really like the way Golang is described in Jon Bodner’s “Learning Go”, namely: “Go is a practical programming language”. That is exactly how we should treat it, in a practical and efficient way, without trying to narrow it and beside ourselves to one concept.
What the heck are pointers?
This is the exact question I asked myself when I saw them for the first time. If you are programming in C or C++, you are more than familiar with this concept. But for people who come from Java or Python, this can be something new.
Basically, a pointer is a variable that stores a memory address to another variable. Usually, pointer stores address a structure of any type. It is worth noticing that pointers always allocate the same amount of memory, regardless of the type they are pointing at.
Go, as usual, does it in its own way, but it is hard to miss that the C-languages family inspires the solution. Unlike them, it does not allow direct manipulation of memory (unless we really, really want to do it) and provides a garbage collector mechanism, which takes away the necessity of memory management, which can often be problematic.
Initially, I felt slightly overwhelmed with pointers syntax and how to use it. Then I realized that they are equivalent to Java references and can be used to imitate the behavior of classes. And they are, actually, not as scary as they seem to be.
First of all, all types in Golang, similarly to functions, are treated as values. This also applies to structs and nested structures. So basically, all data types, simple and complex, behave like simple types in Java. The following example illustrates it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | package main import "fmt" func main() { i := 3 fmt.Println(fmt.Sprintf("Before modify integer: %d", i)) modifyInteger(i) fmt.Println(fmt.Sprintf("After modify integer: %d", i)) v := Vehicle{ colour: "red", engine: Engine{ capacity: 1.2, cylinders: 4, }, } fmt.Println(fmt.Sprintf("Before modify value: %v", v)) modifyVehicle(v) fmt.Println(fmt.Sprintf("After modify value: %v", v)) } type Vehicle struct { colour string engine Engine } type Engine struct { capacity float64 cylinders int } func modifyInteger(num int) { num = 5 fmt.Println(fmt.Sprintf("Inside modify integer: %d", num)) } func modifyVehicle(v Vehicle) { v.colour = "blue" v.engine.capacity = 3.0 v.engine.cylinders = 6 fmt.Println(fmt.Sprintf("Inside modify value: %v", v)) } |
The output of the following program is:
1 2 3 4 5 6 | Before modify integer: 3 Inside modify integer: 5 After modify integer: 3 Before modify value: {red {1.2 4}} Inside modify value: {blue {3 6}} After modify value: {red {1.2 4}} |
As mentioned earlier, Golang treats all data types as values. And since we pass values, even though they look like “objects,” the modification scope of an integer and vehicle is limited to the method scope. Now let’s look at similar code written in Java.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | package main; public class Main { public static void main(String[] args) { int i = 2; System.out.printf("Before modify integer: %d%n", i); modifyInteger(i); System.out.printf("After modify integer: %d%n", i); Vehicle v = new Vehicle("red", new Engine(1.2, 4)); System.out.printf("Before modify vehicle: %s%n", v); modifyVehicle(v); System.out.printf("After modify vehicle: %s%n", v); } public static void modifyInteger(int num) { num = 5; System.out.printf("Inside modify integer: %d%n", num); } public static void modifyVehicle(Vehicle v) { v.colour = "green"; v.engine.capacity = 3.0; v.engine.cylinders = 6; System.out.printf("Inside modify vehicle: %s%n", v); } } class Vehicle { String colour; Engine engine; public Vehicle(String colour, Engine engine) { this.colour = colour; this.engine = engine; } @Override public String toString() { return String.format("{%s %s}", colour, engine); } } class Engine { double capacity; int cylinders; public Engine(double capacity, int cylinders) { this.capacity = capacity; this.cylinders = cylinders; } @Override public String toString() { return String.format("{%.1f %d}", capacity, cylinders); } } |
The code above, when run, generates the following output:
1 2 3 4 5 6 | Before modify integer: 2 Inside modify integer: 5 After modify integer: 2 Before modify vehicle: {red {1.2 4}} Inside modify vehicle: {green {3.0 6}} After modify vehicle: {green {3.0 6}} |
As we can observe, the Java code output differs from the one generated by Golang’s snippet. The behavior is the same for the simple types, but it is not the case when it comes to complex Java types. But why, actually? Java and Go are pass-by-value, so theoretically, they should work similarly. As you should know, all complex types in Java (basically all objects) are passed by the reference value. Simply, the reference is a handle to an object, which has its memory address. When calling a method, the value of this reference is passed, and Java automatically links to the actual values in memory that the reference points at.
Therefore, modifications of object properties inside the method change values of the original properties. That is why modifications of reference types are beyond the scope of the method. While Java does all of the magic underneath, Go, on the other side, requires you to declare it explicitly. To replicate the same behavior, we have to use pointers. Pointers have the same role as Java references, which are values that hold an address to the original value. To declare that we want to operate on a pointer, we must mark the input parameter with an asterisk (“*”). After the implementation of pointers in the previous code snippet, the changed method will look following:
1 2 3 4 5 6 | func modifyVehicle(v *Vehicle) { v.colour = "blue" v.engine.capacity = 3.0 v.engine.cylinders = 6 fmt.Println(fmt.Sprintf("Inside modify value: %v", *v)) } |
Please note that the asterisk symbol also prepends variables passed to the string format. When a variable is marked with the asterisk symbol, it is dereferenced. Dereferencing is an operation that converts pointer parameters to a value type, basically an action opposite to getting a pointer of value. So, how to get a pointer? We must use an ampersand (“&”) on the value type. Our Golang example, after the necessary changes, will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package main import "fmt" func main() { v := Vehicle{ colour: "red", engine: Engine{ capacity: 1.2, cylinders: 4, }, } fmt.Println(fmt.Sprintf("Before modify value: %v", v)) modifyVehicle(&v) fmt.Println(fmt.Sprintf("After modify value: %v", v)) } type Vehicle struct { colour string engine Engine } type Engine struct { capacity float64 cylinders int } func modifyVehicle(v *Vehicle) { v.colour = "blue" v.engine.capacity = 3.0 v.engine.cylinders = 6 fmt.Println(fmt.Sprintf("Inside modify value: %v", *v)) } |
And produce the following output:
1 2 3 | Before modify value: {red {1.2 4}} Inside modify value: {blue {3 6}} After modify value: {blue {3 6}} |
As you can see, with the help of pointers, we were able to modify the properties of the structure permanently. Using pointers, we can simulate the nature of Java classes. Using that concept, we can distinguish two function types in Golang: the first type is used for performing operations and calculations, and the second type is for changing the state of the” object.” And that can be done simply by changing the signature of input and/or output parameters.
What is worth remembering is syntax, which is used to switch operation scope between value and reference types. In summation:
- Mark value type with & to get the pointer,
- Mark reference type with * to get value,
- Signature marks with * means that we are operating with pointers.
As I mentioned at the beginning, pointers are a vast and complex topic, and we have only scratched the surface of this topic, but it should be enough to get started.
Let’s stay a little bit longer in an area connected with OOP and talk about…
Encapsulation
Looking at the previous examples, you have probably noticed that in all Golang code examples, we did not use any access modifiers. But did we? Actually, we were using them all the time, but unlike Java, there are no special keywords to do that. Here is a quick reminder for those who like to refresh their knowledge about access levels in Java.
So what access modifiers do we have in Golang? There are two of them:
- Upper case – every field and function, which name starts with an uppercase letter, is equivalent to a public Java modifier. It means that, as long as the package is imported, we can access and use them freely. Golang defines such functions and values as exported names and functions.
- Lowercase – fields, and functions, which names start with a lowercase letter, behave like package-protected (no modifier) access levels in Java. It means they can be freely accessed and modified within the same package. Unexported fields and structures can, however, be initialized and accessed outside their home package.
To visualize how it works, let’s look at the following example. It represents a very simple furniture shop application, and it consists of three modules:
- Cargo – this module is used to calculate the price of transportation of furniture if a customer decides to order such an option. Below is a code snippet of the cargo file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | package cargo import ( "math" ) const DISCOUNT_FACTOR float64 = 0.9 const highRate float64 = 1.3 const mediumRate float64 = 1.1 const lowRate float64 = 1.0 func NewParcel(length int, width int, height int, weight float64) parcel { return parcel{ length: length, width: width, height: height, weight: weight, } } type parcel struct { length int width int height int weight float64 price float64 } func (p parcel) CalculatePrice() float64 { cubature := p.calculateVolume() if cubature < 0.8 { if p.weight < 30 { p.price = cubature * p.weight * highRate } else { p.price = cubature * p.weight * mediumRate } } else { if p.weight < 60 { p.price = cubature * p.weight * mediumRate } else { p.price = cubature * p.weight * lowRate } } if p.height == p.length && p.height == p.width { p.applyDiscount() } return math.Round(100*p.price) / 100 } func (p parcel) calculateVolume() float64 { return float64(p.length*p.height*p.width) / (100 * 100 * 100) } func (p parcel) applyDiscount() { p.price = p.price * DISCOUNT_FACTOR } |
As you can see, the structure of the parcel, its parameters, and several methods are private to hide details of implementation and prevent data manipulation, especially price 🙂
We have two exported functions: NewParcel and CalculatePrice. NewParcel is a creator method for parcel type, and CalculatePrice is a parcel type method to calculate the transport price. You may ask, why does an unexported type need exported methods? The answer is easy, namely, to guarantee encapsulation. As mentioned earlier, unexported types can be initialized and accessed outside their package. And that is the way it can be done.
- Furniture – this module is a trivial implementation of equipment that the store has to offer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | package furniture func OrderBed(colour string) furniture { return furniture{ Length: 200, Width: 160, Height: 50, Weight: 113.6, price: 400, colour: colour, } } func OrderChair(colour string) furniture { return furniture{ Length: 60, Width: 60, Height: 60, Weight: 5.2, price: 50, colour: colour, } } func OrderCabinet(colour string) furniture { return furniture{ Length: 80, Width: 40, Height: 180, Weight: 17.3, price: 170, colour: colour, } } type Furniture interface { Price() float64 Dimensions() (int, int, int, float64) } func (f furniture) Price() float64 { return f.price } func (f furniture) Dimensions() (int, int, int, float64) { return f.Length, f.Width, f.Height, f.Weight } type furniture struct { Length int Width int Height int Weight float64 price float64 colour string } |
The module file consists of furniture structure. Some of its fields are exported, though they should not in this case, while others are unexported. Such a combination is acceptable and can be used if implementation requires it. Our struct has three creator and two getter methods: Price and Dimensions. Getters in Golang, according to Go’s documentation, should not contain “Get” phrase in the method name, as it is unidiomatic. More information can be found here.
The module also consists of exported interface Furniture, which declares furniture structure-getter methods.
- Invoice – its file consists of one function, Generate, which is used to calculate the overall price of the order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package invoice import ( "encapsulation/src/cargo" "encapsulation/src/furniture" ) func Generate(furniture furniture.Furniture, includeTransport bool) float64 { fmt.Printf("Inside invoice.Generate method: %v \n", furniture) price := furniture.Price() if includeTransport { cargo := cargo.NewParcel(furniture.Dimensions()) price = price + cargo.CalculatePrice() } return price } |
It is a simple method, yet there is one interesting thing about it. We pass a Furniture interface as a function parameter to access furniture struct methods. Without the use of an interface, it would not be possible to pass an unexported struct as a method signature. Because of the interface, we can pass furniture created with the creator method and use its methods, which the interface declares.
- Main – it is just the starting point of our application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package main import ( "encapsulation/src/furniture" "encapsulation/src/invoice" "fmt" ) func main() { item := furniture.OrderBed("blue") // item.Length = 10 // item.Width = 10 // item.Height = 10 toSettle := invoice.Generate(item, true) fmt.Printf("Generated price: %.2f", toSettle) } |
As mentioned earlier, we can build an unexported struct in another package with the creator method. Moreover, we can modify the exported parameters of an unexported struct. In this example, we can modify the furniture dimensions, affecting the cargo service’s price. Let us run the application and see its output:
1 2 | Inside invoice.Generate method: {200 160 50 113.6 400 blue} Generated price: 581.76 |
In the first line printed, we can see all of the unexported values of furniture. Though they seem inaccessible, it is possible to get them using reflect utils.
Below there is a program output after uncommenting lines in the main file:
1 2 | Inside invoice.Generate method: {10 10 10 113.6 400 blue} Generated price: 400.12 |
Unsurprisingly, the modified values passed to the method generated the wrong output. Of course, this example is very basic, even naive. Yet, it is worth emphasizing that it is possible to modify exported names, which are a part of unexported structures. We should be fully aware of how to control which variables we want to share with the outer world.
Conclusion
Golang is a very powerful and flexible programming language that is worth knowing. It is popular and has a strong community, yet I feel that its popularity will grow over time. The article shows that it is relatively easy to learn and provides a wide range of applications. Moreover, it has some advantages over Java. So do I recommend switching from Java to Golang?
Well, it depends. Java is well-documented, has tons of support, and is by far the most popular programming language. Spring is excellent and makes enterprise applications easy to write and maintain. If you have a huge piece of software to write, which requires the work of a dozen developers, there is no point in blazing trails unless there is a good reason.
But there is one aspect, which could be a bargaining chip, and that is resource usage. JVM is quite heavy, especially with a Spring. It requires at least 256 megabytes of RAM to spin up the “Hello world” application. The same program in Golang will consume about 15 megabytes. We live in times of powerful machines with lots of computing capabilities, and we do not have to worry about running out of RAM. Yes, that is true, but then the money talks. Why spend lots of cash to scale up when a different choice can save us plenty from the beginning? With the resources necessary for one Java pod, it is possible to host at least a few equivalent pods written in Golang.
That is why I predict that Golang will become even more popular in the future.