go |java

Golang through the eyes of a Java developer - pros and cons

mraszplewicz

Maciej Raszplewicz

Posted on December 16, 2020

Golang through the eyes of a Java developer - pros and cons

Recently I had to learn the Go programming language and now I want to share my thoughts from the perspective of a Java developer.

We decided to create Kubernetes operators for the DevOpsBox (https://www.devopsbox.io/) platform (read more about our reasons: https://dev.to/mraszplewicz/my-perfect-aws-and-kubernetes-role-based-access-control-and-the-reality-48fb). It turns out that it is easiest to create them in Golang - there are Kubebuilder and Operator SDK frameworks. The only thing is that we didn't have Golang skills, so I had to learn a new language...

I will start with things I like and move to those I don't. I will try to focus on the language itself.

Things I like

Easy to learn

It is amazing how easy it is to learn the Golang. A Tour of Go (https://tour.golang.org/) covers almost every aspect of the language and the language specification (https://golang.org/ref/spec) is reasonably short and readable. There are some not-so-easy-to-understand features like channels, but they are powerful and there is a reason why they exist.

Before version 5, Java didn't have so many features either, but it has never been as simple as Golang is now.

A fast developer feedback loop

By using the term "developer feedback loop", I mean the time from starting the program to seeing its results.

Sometimes you expect that you have to wait a long time to start a program written in a compiled language. Go main or unit tests start almost instantly when run from the IDE, so the feedback loop can be very short. Results are similar to those in other modern programming languages and even though Go is statically compiled, you don't have to wait long for the compilation process.

It is funny that nowadays we have to build programs written in languages that are by design not statically compiled (e.g. JavaScript) and sometimes wait for the build process to complete.

Static type checking

Go has a very good static type checking system. You don't even have to declare a type of every variable, you simply write:

str := "Hello"
Enter fullscreen mode Exit fullscreen mode

and it knows that variable str is of type string. The same is true when the variable is a result of a function call:

result := someFunction()
Enter fullscreen mode Exit fullscreen mode

Implicit interface implementation

"If it walks like a duck and it quacks like a duck, then it must be a duck."
It means that you don't have to explicitly declare that you are implementing an interface. In Go you can write:

type Duck interface {
    Quack()
}

type Mallard struct {
}

func(mallard *Mallard) Quack() {
}
Enter fullscreen mode Exit fullscreen mode

Mallard is a duck because it can quack! You can write:

func main() {
    var duck Duck
    var mallard *Mallard

    mallard = &Mallard{}
    duck = mallard

    duck.Quack()
}
Enter fullscreen mode Exit fullscreen mode

In Go, you can create your own interfaces even for the existing external code.

Multiple return values

This one is simple - you can just return multiple values from a function:

func Pair() (x, y int) {
    return 1, 2
}
Enter fullscreen mode Exit fullscreen mode

In most languages I know, you will have to create a class or struct to achieve something similar.

But this feature is also used to return errors from functions - more about it in "Things I don't like".

Function values

Functions can be passed as parameters, assigned to variables, etc.:

func doJob(convert func(int) string)  {
    i := 1
    fmt.Print(convert(i))
}

func main() {
    convert := func(intVal int) string {
        return strconv.Itoa(intVal)
    }
    doJob(convert)
}
Enter fullscreen mode Exit fullscreen mode

It is somehow similar to method reference or lambda expressions in Java, but more "built-in" into the language.

Modules

Go has a really good built-in dependency management system. You don't need Gradle or Maven to download dependencies, you just use Go modules.

Unit tests

Unit test support is a part of the language itself. You just create a file with the "_test.go" suffix and write your tests. For example, for hello.go:

package hello

func sayHello() string {
    return "Hello world!"
}
Enter fullscreen mode Exit fullscreen mode

you create hello_test.go file:

package hello

import "testing"

func TestSayHello(t *testing.T) {
    greetings := sayHello()

    if greetings != "Hello world!" {
        t.Errorf("Greeting is different than expected!")
    }
}
Enter fullscreen mode Exit fullscreen mode

and you can just run it from your IDE or in CI/CD pipeline.

If you want to write good tests in Golang, you should probably write "table-driven tests". A good article on this topic: https://dave.cheney.net/2019/05/07/prefer-table-driven-tests

Defer

Go provides something similar to Java's finally keyword but, in my opinion, more powerful and simpler. If you want to run some code when your function returns, for example, to clean up some resources, you use the defer keyword:

func writeHello() {
    file, err := ioutil.TempFile(os.TempDir(), "hello-file")
    if err != nil {
        panic(err)
    }
    defer os.Remove(file.Name())

    file.WriteString("Hello world!")
}
Enter fullscreen mode Exit fullscreen mode

Here we create a temporary file that will be removed at the end of the function execution, just after writing "Hello world!".

Single binary

Go is famous for producing a single binary. When you build your program, you will get a single executable file containing all the dependencies. Of course, you have to prepare a separate binary for every target platform, but the distribution of your program is simpler compared to other languages. This is one of the reasons why Go is often used for creating command-line utilities.

Cobra library

https://github.com/spf13/cobra is the second reason... It is an extremely useful library, which helps to write command-line tools. Having created our operators in DevOpsBox, we also wanted to have our own CLI and it was a pleasure to write them using Cobra.

Things I don't like

Nothing is perfect and Go is no exception, it has some features that I don't like that much...

Error handling

I really don't like Go idiomatic error handling. There are a few main reasons why:

Error handling makes the code less readable

Often in Go programs, you see something like this:

func doJob() ([]string, error) {
    result1, err := doFirstTask()
    if err != nil {
        log.Error(err, "Error while doing the first task")
        return nil, err
    }

    result2, err := doSecondTask()
    if err != nil {
        log.Error(err, "Error while doing the second task")
        return nil, err
    }

    result3, err := doThirdTask()
    if err != nil {
        log.Error(err, "Error while doing the third task")
        return nil, err
    }

    return []string{
        result1,
        result2,
        result3,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

I think that error handling adds a lot of noise here. Without it, this function would look like this:

func doJob() []string {
    result1 := doFirstTask()
    result2 := doSecondTask()
    result3 := doThirdTask()

    return []string {
        result1,
        result2,
        result3,
    }
}
Enter fullscreen mode Exit fullscreen mode

Java, also, has some issues - namely checked exceptions, which add a lot of unnecessary noise. Thankfully, there are frameworks like Spring Framework, which wraps checked exceptions into runtime exceptions, so you can catch only those that you expect.

You can forget about handling an error

Consider this code:

func doJob() ([]string, error) {
    result1, err := doFirstTask()
    if err != nil {
        log.Error(err, "Error while doing the first task")
        return nil, err
    }

    result2, err := doSecondTask()

    result3, err := doThirdTask()
    if err != nil {
        log.Error(err, "Error while doing the third task")
        return nil, err
    }

    return []string {
        result1,
        result2,
        result3,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

What's wrong with it? I forgot to handle an error! In a little bit more complicated cases, it is possible to miss it while doing a code review.

In other programming languages, you would have centralized exception handling and stack traces available for debugging purposes.

You don't have your stack trace

I have read some articles about error handling in Golang and there are opinions that stack traces are "unreadable, cryptic", but I got used to them and for me, it is easy to find a problem with help of a stack trace.

Problems with refactoring

IDEs have some issues with refactoring Golang code, for example, when extracting a method or a variable. I think that these problems are related to idiomatic error handling.

Sometimes too brief

Sometimes I feel that Golang is too brief. Why do we have keywords like func, not function? Why are we not forced to use surrounding parentheses in if or for? Why don't we have any keyword like public and we have to declare upper case instead? But it is probably only my personal opinion...

Sometimes inconsistent

Go does not support function/method overloading (https://golang.org/doc/faq#overloading). I am ok with that because there are many programming languages without it, but why does the built-in make function have many variants? Looking at the documentation:

Call             Type T     Result

make(T, n)       slice      slice of type T with length n and capacity n
make(T, n, m)    slice      slice of type T with length n and capacity m

make(T)          map        map of type T
make(T, n)       map        map of type T with initial space for approximately n elements

make(T)          channel    unbuffered channel of type T
make(T, n)       channel    buffered channel of type T, buffer size n
Enter fullscreen mode Exit fullscreen mode

Features hard to remember

It is probably the case of any programming language, but Go is so simple that I thought I would easily remember how to use all the keywords and all the built-in functions. Unfortunately, the reality is quite different. After a year of using the language on a daily basis, I still need to look into the documentation or check examples to find how to use channels, or how to use make. Maybe I have too many different programming languages in my mind...

No streaming API equivalent

There are some features in other languages that let you interact with collections using declarative syntax, like streaming API in Java. On the other hand, in Golang you won’t find anything similar (or maybe there is some library?) and it is idiomatic to use for loops. Loops aren't that bad, but I like the streaming API and got used to it.

Not that good IDE support

I love JetBrains products and I use GoLand to write Go code. I have already mentioned issues with refactoring and here is an example. If you want to extract a method from this code:

func doJob() ([]string, error) {
    result1, err := doFirstTask()
    if err != nil {
        log.Error(err, "Error while doing the first task")
        return nil, err
    }

    result2, err := doSecondTask()
    if err != nil {
        log.Error(err, "Error while doing the second task")
        return nil, err
    }

    result3, err := doThirdTask()
    if err != nil {
        log.Error(err, "Error while doing the third task")
        return nil, err
    }

    return []string {
        result1,
        result2,
        result3,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

GoLand will do it like this:

func doJob() ([]string, error) {
    result1, err := doFirstTask()
    if err != nil {
        log.Error(err, "Error while doing the first task")
        return nil, err
    }

    result2, result3, strings, err2 := doThirdAndSecond(err)
    if err2 != nil {
        return strings, err2
    }

    return []string{
        result1,
        result2,
        result3,
    }, nil
}

func doThirdAndSecond(err error) (string, string, []string, error) {
    result2, err := doSecondTask()
    if err != nil {
        log.Error(err, "Error while doing the second task")
        return "", "", nil, err
    }

    result3, err := doThirdTask()
    if err != nil {
        log.Error(err, "Error while doing the third task")
        return "", "", nil, err
    }

    return result2, result3, nil, nil
}
Enter fullscreen mode Exit fullscreen mode

which is not that bad, but requires additional manual fixes. IntelliJ does a better job for Java!

Conclusion

Although there are a lot of pros of Golang, cons are significant and therefore I have mixed feelings about the language. I can say that I like it, but it will probably never be my favorite. So which one is? C#, but that is a completely different story… I think it is a personal matter who likes which programming language, and it is all for good!

💖 💪 🙅 🚩
mraszplewicz
Maciej Raszplewicz

Posted on December 16, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related