To Go or not to Go

Throughout my career, I have worked with a variety of programming languages. The majority of them have similar concepts, with the main difference being in the structure, syntax, and eco-system (compiler, libraries, etc). I enjoyed writing code in C and C++ at Dell, and when I joined Velostrata (which was acquired by Google), I was thrilled to switch to C++ 11, which added a lot of excellent functionality and eliminated the need for external libraries (like BOOST). I also used Python for small utilities and testing on occasion. At AWS, I completely switched to Java. I learned to appreciate the fully managed eco-system, complete with a GC that looks after your memory and thousands of libraries that help you save time and focus on your business logic. Before joining Highline, I was informed that our primary programming language is Go, and I was eager to take on the challenge of learning a new language πŸ™‚ Here are my thoughts after a few months of writing in Go.

Initially, I began looking for an editor. I was familiar with JetBrains and had used their products before (CLion, PyCharm, IntelliJ). Although they offer a Go IDE (called GoLand), I discovered that there is no free version, and I was unwilling to pay so much out of pocket unless there was a compelling reason. I chose VSCode based on the team’s positive recommendations. After remembering it being heavy and slow a few years ago, I was surprised to see it running fast, and it even had an official Golang plugin that made writing code, running it, and debugging simple. So, I guess I’ll stick with VSCode for the time being. Yes, it has a few quirks, particularly with the unit tests, but nothing major that would irritate me too much.

The Good

One of the great things is the speed and simplicity. With a few lines of code, you can start an HTTP server, add a few endpoints, and route them to specific functions. You can use middleware modules to intercept requests, and structure your application in a few easy steps. There are also ready-to-go libraries to work with SQL (and non-SQL) databases (e.g. Gorm), define and emit metrics to Prometheus (to eventually integrate them into Grafana dashboards) and many more. There is probably a library out there that you can leverage and use almost everything you need.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/hello", HelloServer)
    http.ListenAndServe(":8080", nil)
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
}

Running it is as simple as executing: “go run hello.go” (assuming this is your file name). Then, from another terminal session making a request will give us the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  Projects βœ— curl -i http://localhost:8080/    
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sun, 04 Sep 2022 21:08:55 GMT
Content-Length: 19

404 page not found

➜  Projects βœ— curl -i http://localhost:8080/hello
HTTP/1.1 200 OK
Date: Sun, 04 Sep 2022 21:08:58 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

Hello World!%                                                                              

➜  Projects βœ—

Go also has support for pointers, allowing you to have better control of your memory and the way things are being passed between functions. Go also has a GC that runs in the background and is cleaning up stuff that you don’t use anymore.
Running functions in the background is also a simple task, and requires you to only add the keyword “go”. A small example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
    "fmt"
    "time"
)

func main() {
    go printNumbers()
    time.Sleep(3 * time.Second)
}

func printNumbers() {
    for {
        fmt.Println("I am running in the background!")
        time.Sleep(500 * time.Millisecond)
    }
}

Running it will produce the following output:

1
2
3
4
5
6
7
8
➜  Projects βœ— go run hello.go
I am running in the background!
I am running in the background!
I am running in the background!
I am running in the background!
I am running in the background!
I am running in the background!
➜  Projects βœ—

When required to pass data between different threads, you don’t have to think about locks and other mechanisms, but just use channels and let it take care of everything else.

The Bad

OOP. Seriously. After working with C++ and Java for so many years, it’s hard for me to accept a modern programming language that doesn’t provide object-oriented capabilities. Well, kinda. Go allows you to define structures, and then you can create methods (so there is a similar notion to “this”), but it doesn’t allow you to do function overloading, and inheritance is replaced by composition (you can include another type within your type) and you can leverage interfaces to do boxing and unboxing. There is no notion of “private”, but everything that starts with a lowercase letter will be considered to exist in the “package” scope, and everything that starts with an uppercase letter will be considered to be public.

Here is an example:

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"

type Name struct {
    First string
    Last  string
}

// This is part of the Stringer interface
func (n Name) String() string {
    return fmt.Sprintf("%s %s", n.First, n.Last)
}

// Note that Name can also be a pointer.
func (n *Name) private() {
    fmt.Println("Ha! You can call me only from within the package")
}

type Person struct {
    Name  // Composition of structs. Note that String() will be inherited
    Phone string
}

func (p Person) DoSomething() {
    fmt.Printf("%s is doing something...\n", p.Name.First)
}

func main() {
    p := Person{
        Name:  Name{First: "Alexander", Last: "Sirotin"},
        Phone: "N/A",
    }

    // Name.String() will be invoked here as we don't have Person.String() implementation.
    fmt.Println(p)

    // Although the function name starts with a lowercase, I can still call it from the outside.
    p.private()

    // You can box structs into interfaces with a specific set of actions
    var boxed interface{ DoSomething() } = p
    boxed.DoSomething()
}

Running it will produce the following output:

1
2
3
4
5
➜  Projects βœ— go run hello.go
Alexander Sirotin
Ha! You can call me only from within the package
Alexander is doing something...
➜  Projects βœ—

Another thing is unit tests. Writing unit tests in Java was a real pleasure, especially with the JUnit5 framework. It allows you to create classes and methods, and have different annotations to control the flow. @BeforeClass, @Before, @After, etc. There is also something that is called Parameterized tests, which allows you to invoke the same test method, using different inputs that you feed from a “method source”. Unfortunately, I haven’t found anything similar in Go out of the box. Yes, there is a powerful testing framework that gives you a LOT of flexibility, as you can write tests and subtests, and do different kinds of stuff, but it doesn’t give you much structure, which makes it easier to have test methods that span over hundreds of lines of code and include different initializations and sub-tests that cannot run (or be debugged) independently. Enforcing a structure takes some effort, and without that, it can become a big mess real quick.

The Ugly

Error Handling. With Go, we are back to the dark ages of returning errors from functions and having to check them after almost every function call. Go doesn’t provide you with an exception mechanism, but instead, it asks you to keep passing “error” objects between function calls. Yes, it has the keywords “panic” and “recover”, which can simulate exceptions-like behaviour, but the common practice is to just propagate errors up the stack.

Let’s take a simple example of the two approaches. With the first approach, where we keep propagating the error, the code is inflated and looks 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
35
36
37
38
39
40
package main

import "fmt"

func main() {

    err := run()
    if err != nil {
        fmt.Printf("Something went wrong: %s\n", err)
        return
    }

    fmt.Println("Completed successfully!")
}

func run() error {

    v, err := load()
    if err != nil {
        return err
    }

    n := calculate(v)
    return persist(n)
}

func load() (int, error) {
    fmt.Println("Loading")
    return 5, nil
}

func calculate(value int) int {
    fmt.Println("Calculating")
    return value * 2
}

func persist(new_value int) error {
    fmt.Println("Persisting")
    return fmt.Errorf("Could not persist the value")
}

Running it will produce the following output:

1
2
3
4
5
6
➜  Projects βœ— go run hello.go
Loading
Calculating
Persisting
Something went wrong: Could not persist the value
➜  Projects βœ—

On the other hand, if we use “panics”, the code 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
35
36
37
38
39
40
41
42
package main

import "fmt"

func main() {

    if ok := run(); !ok {
        return
    }
    fmt.Println("Completed successfully!")
}

func run() bool {

    defer func() {
        fmt.Println("Recovering")
        if r := recover(); r != nil {
            fmt.Printf("Something went wrong: %s\n", r)
        }
    }()

    v := load()
    n := calculate(v)
    persist(n)
    return true
}

func load() int {
    fmt.Println("Loading")
    return 5
}

func calculate(value int) int {
    fmt.Println("Calculating")
    return value * 2
}

func persist(new_value int) {
    fmt.Println("Persisting")
    panic("Could not persist the value")
    fmt.Println("Persisted")
}

Running it will produce the following output:

1
2
3
4
5
6
7
➜  Projects βœ— go run hello.go
Loading
Calculating
Persisting
Recovering
Something went wrong: Could not persist the value
➜  Projects βœ—

Note that we still need to have a return value from “run()” to indicate whether we should continue with the flow or not, and there is still overhead with writing the recovery function (“defer” is a nice keyword in Go that basically says, run this when going out of scope, regardless of what happened). Using panics as a pattern, and forgetting a “recover” statement may crash your whole application, even if it happened in one thread.

As mentioned before, the most common pattern in Go is to return errors, and most of the libraries I worked with are following it, which makes the code long and messy.

In Conclusion
Go is nice πŸ™‚ Yes, it has some quirks, but the bottom line is that it’s a powerful modern language which provides you with great tools to focus on the business logic of your application, and with great performance. The language is still evolving, and version 1.19 has some kind of “Generics” and I am confident that it will become even more powerful with time.

– Alexander

close

Oh hi there πŸ‘‹
It’s nice to meet you.

Sign up to receive awesome content in your inbox, as soon as it is published!