DEV Community

Cover image for Golang through the eyes of a Java developer - pros and cons
Maciej Raszplewicz
Maciej Raszplewicz

Posted on • Edited on

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!

Top comments (23)

Collapse
 
robindiddams profile image
Robin Diddams

I write a lot of Go, I agree with most of your assessment, although I think Go was never really designed to live in an IDE, I use vscode but havent touched the refactor stuff, the Go cli to me is home 🏠.

Also I disagree that returning errors makes the code more unreadable, less pretty sure, but not more confusing, I think if youre used to wrapping everything in try-catches and explicitly defining all errors in and out of your functions then Go's way may seem repetitive, but fundamentally it's simpler, call a function get some value(s) back, theres no extra things popping out at inconvenient times (unless it panics😅), I find this to be a lot easier to explain to beginner developers. Again I wouldn't say it looks better, but I think its pretty clear what's going on.

Collapse
 
alainvanhout profile image
Alain Van Hout

I think the main point is that with exceptions you don't need lots of try-catch blocks all over the place. You only add them where they are relevant, while the rest of the code can be unconcerned with them, which isn't the case when you need to bubble up an error.

Collapse
 
mraszplewicz profile image
Maciej Raszplewicz

That is exactly my point. If I don't need to check/expect an error, I just don't do that and I know the program/thread will crash with a stack trace. If I don't have to write all the error handling and return them to the calling function, my code becomes cleaner and easier to read. Like in this example:

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

    return []string {
        result1,
        result2,
        result3,
    }
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
robindiddams profile image
Robin Diddams

wait so you call functions that could throw exceptions but dont handle them until you find that they do throw? I was under the assumption you would still always check but would just prefer to do that check at a higher level (ie. wrap doJob in some error handling), but if you want to ignore errors just don't check them:

// assuming doNTask returns a string and an error
func doJob() []string {
    result1, _ := doFirstTask()
    result2, _ := doSecondTask()
    result3, _ := doThirdTask()

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

lmk if im not understanding you correctly

Thread Thread
 
mraszplewicz profile image
Maciej Raszplewicz

Of course, it is not that I totally ignore all errors/exceptions, I want to handle them (as you have written) "at a higher level". Probably also I don't always know all the exceptions that can be thrown.

And if I ignore an error like in your example, my program will continue to work i.e. all functions doFirstTask, doSecondTask and doThirdTask will be executed. I want my doJob function to fail after the first error. I can achieve that with panic but it is not a good solution too and not idiomatic.

Thread Thread
 
alainvanhout profile image
Alain Van Hout • Edited

Think of it like this: your code is made up of lots of functions calling lots of other functions that are calling lots of other functions. If something goes wrong somewhere, then it's not necessarily the function right above that one that's best placed to handle the error. A thrown exception allows the error to bubble up automatically until the level on which it makes sense to handle the error.

For example, if I have a complex algorithm that at some point does a division by zero, then the code calling the complex algorithm will need to e.g. use a fallback or notify the user of the error. In either case, it's not the algorithm function right above the error that is able to do anything useful with the division by zero error.

Collapse
 
robindiddams profile image
Robin Diddams

I see what you mean, in cases where I don't want to check and wrap an error that would be convenient

Collapse
 
martinhaeusler profile image
Martin Häusler

Go is a really weird one. I was excited at first, because I'm severely lacking a low-level programming language in my personal portfolio (I can do C if I have to, but I don't like it because it's so blatantly unsafe). But then I tried it and... oh man. The error handling is straight out of the previous millenium (by now, we do have fancy things like, you know, try-catch-finally...) and the language has many awkward choices. Back then when I used it you were still limited to a single GOPATH (I've heard that's different now) and package management (in particular: package version management!) wasn't even close to the reliability and quality I was used to in Maven / Gradle.

My absolute worst experience with Go was when I had to parse some JSON data into a struct, analyze it, update it, and serialize it back. It was such a pain. Oh yeah, and no generics on top.

Go does have some stuff going for it though. Being able to produce an executable for any desired target platform independent of the system you're currently running on is impressive. Build times are quite fast and modularity is built-in from the start. The interface duck-typing is something I can get behind because it's compiler-checked and an interesting approach. And the compiler is really really rigid when it comes to code style - an unused variable is an error. Those things I remember as positives, but overall I'll stay away from it for the time being.

Collapse
 
aminmansuri profile image
hidden_dude

Frankly, I love checked exceptions. I think they are a great feature. They're just misunderstood and maligned by Bruce Eckel who didn't get them.

But they make my code much cleaner because I use them properly.

It's fine if some languages are not as strict with types or errors than others. But that was Java's philosophy and I have no problem with it.

Putting if/else error checking is definitely a step backward.

I guess a better way to do error handling in Go would be to return a "result" object that would have an "value()" method to return the value, or an "ifError()" method to handle the error with a lambda.

But the nice thing about exceptions was being able to largely forget about them and let them trickle up to some handler. But it seems to be a hard concept for some people for some reason. Error codes aren't better.

Collapse
 
bias profile image
Tobias Nickel

Thanks for this article, I was reading it, as js/ts developer, who also know a thing or two about golang. (and usually dislike java).

I can't agree more about the aweful error handling. every second line checking for an error.

Once I was writing unit tests, for an existing project. I was using an sql driver mock. It feeled very cumbersome, not productive. Along the way I did some refactoring. First doing the change I wanted, then recompile and run the test as long as everything got into its place again. It was very much guided by golang. (statically types language). While I did not feel productive, only fixing what the compiler tell me, (can't the compiler fix the code for me),... but it absolutely feeld satisfying in the end. and I was a little proud when that work was done.

Collapse
 
marcello_h profile image
Marcelloh

"Error handling makes the code less readable"
You can make it ja biut more like your example:
result1, err1 := doFirstTask()

Stacktraces are possible

I always refactor by hand because then, it comes out the way I want. (and it's not that much more effort)

Collapse
 
alainvanhout profile image
Alain Van Hout

I'm curious. Refactoring is a completely deterministic process that requires some effort and attention to detail but no creativity. Because of that, it's also easy to make mistakes by forgetting a detail or due to a copy/paste error.

In other words, it's a perfect task to be automated. In what way do you not get it exactly as you want it, when e.g. extracting a variable or a method?

Collapse
 
marcello_h profile image
Marcelloh

Refactoring in my eyes is not only to move parts of a complexer function out of there, but rethink why the function was complex in the first place. This kind of refactoring cannot be automated.
I daily check my code for complexity of functions (automated) and refactor when I spot a candidate, because then, it's still fresh in my memory.

Thread Thread
 
alainvanhout profile image
Alain Van Hout

That sounds reasonable, and obviously not something a computer can do for you.

I tend to use the term 'rework' for that, while sticking to the traditional meaning for 'refactor'. But as long as all parties understand what is meant, that doesn't really matter 😄.

Collapse
 
kilaka profile image
Alik Elzin • Edited

Love the Java Perspective of the Go language.
It feels like Go is the next step for low level languages (C/C++), but the creators forgot some great high level language features.
This makes Go's target applications to be command line executors and independent algorithms.
If Go had embraced some high level concepts like runtime exceptions, JPA, annotations (Spring boot), generics, we could have written any type of app we wanted, with any large scale team of numerous levels of developers.

Collapse
 
andreidascalu profile image
Andrei Dascalu

You do realize that many of these points have nothing to do with the language itself, right?

You don't have to return errors if you don't want/need it. Second version of your function is ok too. The idiomatic handling is verbose, sure but whether or when you need it is up to you. There are plenty of cases when you wouldn't want to return anyway (eg: from goroutines)

Also, your not handling error example is a side effect of reusing variables. I advise against it on principle and I find it sad that Go allows it.

Goland has shortcomings for such an expensive product but many people prefer VSCode for that. I find significantly better than Goland IMHO. But it's weird to say Go has poor IDE support. Did you try anything not by Intellij?

Why parenthesis are not enforced around ifs? Why would they be? You need a reason to use a syntax element not to not need one. Semicolons also exist but are not enforced unless you have multiple statements on a single line.

I find the error handling tedious but not unreadable. It's actually very readable imho, more so than a catch/throw system where the actual handling may be elsewhere. I look at a file and know exactly what happens to my errors (and I can handle only what I want to handle)

Collapse
 
mraszplewicz profile image
Maciej Raszplewicz

Thanks, for your comment. I really appreciate it, even if you don't agree. It is always possible to learn something new.

First of all, my reception of the language is mostly positive.

I know that I can handle errors in different ways, but it will not be idiomatic, most libraries do return errors, so I will have to wrap functions from some lib into my own. So different way (i.e. using panic) is not that good too...

About not reusing variables - agree, but there are plenty of examples, questions etc. where people do reuse error variables. Maybe they are wrong, but my conclusion is that reusing error variables is idiomatic.

You are right that I haven't tried VSCode (I use it but not for Go programming), now I have. It also has shortcomings... And extracting function refactoring creates worse results than in GoLand - it does not compile.

When it comes to parenthesis, this is what I am used to and find more readable. It is not that important though.

Error handling - my opinion is that it forces you to write less readable code. Catch/throw system without checked exceptions is what I like the most.

We can disagree because we are different, have different experiences etc.

Collapse
 
andreidascalu profile image
Andrei Dascalu • Edited

It's not that I don't agree. In fact, I'm familiar with the ideas as they're nearly the same as what I thought coming into Go from PHP. What I'm talking about is a bit of perspective on approaching a different language (and also a bit about separating what makes a language: aka the qualities of the language itself as separate from the platform, including its tooling and again separate from third party support in the form of IDEs).

Thing is, you should define what "idiomatic" means. From what I infer from conversations, it's mostly "the way of doing things that the language enforces or hints at" which is different than "how most people do it through materials/online howto's". I'm sure you also found that quick stackoverflow solutions and straight-to-the-point articles are hardly a collection of best practices. Reusing (or not) is not part of idiom, in this way. You could say that how most people approach solutions should be considered so (particularly because you'll see this in the stdlib, with the caveat that stdlib itself is a collection of tools and ready-made solutions made by people and hardly perfect) but in that case "idiomatic" loses any value since it's not a benchmark for current best practices and merely a mirror or what the majority o developers chose to do some time ago. Following the idiom in this way precludes progress and I've seen this first hand in PHP: it started as a bunch of useful scripts, then grew into a sort of procedural kind of thing before slowly developing OOP-ish ways. What's considered idiomatic has evolved, even if you'd find plenty of people nowadays that swear by procedural spaghetti code of ye olde days.

Thing is, I also liked the try/catch and lots of other stuff in PHP ... but it took learning a few languages that used different paradigms (ranging from slightly different to "other side of the world" different) to put some things into perspective and reconsider the qualities of what was familiar.

Collapse
 
mkunikow profile image
Michal Kunikowski • Edited

How K8s deals with no generics ... mess or not mess ... 🤔

The core team replaced a compile-time language feature that was missing (Generics) with their home-built runtime system. And given the tools at their disposal, they did a pretty good job.

medium.com/@arschles/go-experience...

Collapse
 
mraszplewicz profile image
Maciej Raszplewicz

Generics are probably going to be added to the language in the future:
github.com/golang/go/issues/43651
blog.golang.org/generics-proposal

The proposal was created on 2021-01-12.

Collapse
 
freedom profile image
Freedom

The less parenthesis is more readable, I always find why other languages tend to use lots and Ruby syntax was cleaner. Go has a well-balanced syntaxes of all languages that it can do many area good enough.

func keyword is derived from B language by Ken Thompson.

Collapse
 
maku profile image
Martin

I tried go, but a language without generics is hard to use...