Posts‎ > ‎

Impressions of Go: a wonderfully simple, weird, native language

posted Dec 28, 2017, 11:09 AM by Renato Athaydes   [ updated Jan 9, 2018, 2:13 PM ]
At my current job, I have joined a team that, among other things, is responsible for creating tools for developers. I'm a long-time JVM (Java/Kotlin/Groovy) developer, but I have to admit that for some kinds of tools we need to create, specially CLIs (command-line interfaces), the JVM (regardless of the language) is not the best choice. That's mostly because of the startup time of the JVM. It has gotten a lot better in recent JVM versions, but there's still a perceptible, even if under-a-second, lag to start even the simplest applications.

For some kinds of tool we might need to develop, a cross-platform, native language can be defintely a better choice.

I've been playing with some native languages like Haskell, Rust and Pony to see if I could find a language that fits that purpose, but unfortunately none of them made me feel satisfied.

Haskell and Rust, while being incredibly powerful tools, are very complex and I found myself being unable to become productive with them within a short time frame (several weeks).
I managed to write a couple of small projects in Rust (JGrab and the unfinished Vinegar), but it felt really constraining to me the whole time, without sign of getting much better with time.

Pony is another really interesting language, with a Python-like syntax, an Erlang-inspired actor-based runtime, a powerful type-system that can encode capabilities which make lock-free concurrency verifiably safe in a way similar to Rust, though using completely different means... but I just don't have the patience to write serious code using it due to the lack of tooling. It's extremely taxing for me to have to keep a browser open to check what method I can call, read basic documentation, read the standard lib code etc. (I've been spoiled by the wonderful tooling one gets to use with Java/Kotlin in IntelliJ).

That's one of the reasons I decided to finally try Go, the language developed by Google that has been a hit in the devops space, with arguably the most respected tools in this space written in Go: Kubernetes, Docker, rkt, several AWS utilities.

Go has been a hit with databases (CockRoachDB, InfluxDB) as well, and many other interesting, groundbreaking projects use Go! Just to give a few more examples of projects mostly written in Go that I felt excited about:

* IPFS - a huge effort towards a decentralized, permanent web.
* Keybase - end-to-end encryption for everyone! Supports chat, a distributed file system (public content can be viewed online) and Git.
* Hugo - static website generator that's becoming hugely popular (I know, I should be using it for this blog... hoping to switch to it soon).

That shows that a lot of cool stuff is being built with Go, which in my opinion is a great reason to give it a try.

Another reason for me, personally, is that Go is actively being used in another of our offices and there's been some talk about trying it in our office as well.

And that's how I got to write a mildly serious project in Go (the project is not ready yet, so I will not publish the link just yet... will update it as soon as I have something useful). UPDATE: the project is now useable and it's a CLI password manager called go-hash.

First impressions


At first look, Go code looks quite familiar, with a C-like syntax. The first not-so--obvious thing I noticed is how variables are declared:



i
:= 0



This declares a variable i with value 0. The type is inferred to be int.

You could also do this, equivalently:



var i int = 0



Or even just this:



var i int



Here, there's two things to understand... first, the type of the variable is declared after the variable name, so here i is the name of the variable, while int is its type.
Second, there's no need to explicitly provide an initial value!

That's because in Go, all types have a zero-value, so if you do not initialize a variable, it just takes that zero-value. For int, the value is, obviously enough, 0.

The next thing I noticed is that in many function declarations, the type of a variable seemed to be omitted:



func
HasPrefix(s, prefix string) bool { /* implementation */ }

 

That means that the variable type is the same as the one following it... so in this example, both s and prefix are of type string. bool is the return type of this function.

You might have noticed that most types start with a lower-case letter, and this function starts with a capital-letter. However, that's not really the case. In reality, any exported symbol in Go needs to start with a capital-letter, otherwise it is private to the scope where it is declared.
An exception to this rule seems to be primitive types, which always start with a lower-case letter, even though they can be used anywhere.

This, for me, is a strange choice because it makes it easy for types and variables names to conflict. I'm used to declare variables (for good or bad) with the same name as their types (but starting with lower-case), which in Go is impossible unless you export your type or the variable. The choice in Go, as the above function (taken from the strings package from the standard lib) shows, seems to be to use one-letter variables instead, which I am not a big fan of.

Anyway, the next thing to notice is that Go does not have classes. Instead, it offers a mix of interfaces and structs, similar to Rust (which has traits instead of interfaces).

A struct, as one might expect from other languages, is just a group of values:



type MyStruct struct {
     enabled bool
     name string
     values []int
}



It is interesting to notice that you can create an instance of a struct without providing values for all (or any) of its fields. The fields are just initialized to their zero-value if no value is provided:



m := MyStruct{
   enabled: true,
 }
 fmt.Printf("%+v\n", m)



Running this code prints this:


{enabled:true name: values:[]}


As you can see, the zero-value of a string is the empty string, and of an array (technically, a slice, actually), the empty array.

Interestingly, nil exists in Go but only pointers can be nil (as the zero-value of a pointer has to be something!) which seems to help limit the damage quite a lot.


The weird bits



The above code brings us to the first weird thing about Go (at least for me): when declaring a struct literal, you must write a trailing comma after each field value, including the last one.

That is, presumably, to be diff-friendly (so adding a new value won't cause the line above it to also change). But it's the first time I see this approach being used... I've seen other languages address this by formatting code differently, for example, Haskell and Elm prefer to just place the comma as the first character of a new line... but anyway, even though this is surprising and a bit weird, there seems to be a justification for it.

If we wanted to initialize the slice, we would need to learn about slice literals and the make built-in.

make is a built-in which cannot be written in Go itself. It can be used to create a slice, map or channel.

So, in order to provide an initial value for the values field of MyStruct, you could either give it a literal value, or use make to just allocate memory for it (in this case, with size 0, capacity 10):



    // literal slice
    m := MyStruct{
        values: []int{1, 2, 3},
    }

    // allocate array for slice with make
    n := MyStruct{
        values: make([]int, 0, 10),
    }



Creating a map with make looks like this (with a map literal also shown):



    // create map using make
    myMap := make(map[string]int)

    myMap["one"] = 1
    myMap["two"] = 2
    myMap["three"] = 3

    // create map literal
    mapLiteral := map[string]int{
        "one": 1,
        "two": 2,
        "three": 3,
    }



Looking at the map declaration above, you might think that Go supports generics. If you've ever read a discussion about Go anywhere, you would know that that's not the case! The Go creators famously refuse to add generics to the language, much to the despair of some people in every discussion around the topic.

The reason maps can be declared in Go is that they are a Go primitive, just like arrays/slices. It's not possible to create your own type that behaves similarly.

That's another weird thing about Go: the amount of constructs available to the user is much smaller than what the language itself makes use of.

So, while Go has no generics, it still has a generic-like thing: map. While there's no overloading in Go, built-ins can still behave as if there were, as in this example with the delete built-in, which removes entries from maps:



    myMap := map[string]int{
        "one": 1,
        "two": 2,
    }

    delete(myMap, "one")

    otherMap := map[int]string{
        1: "one",
        2: "two",
    }

    delete(otherMap, 1)



Declaring a function like delete in user code is just not possible. Notice, though, that unlike in most languages with generics, delete is type-safe: trying to delete a key with the wrong time is a compiler error.

To follow up on the delete built-in, it was very interesting to discover how to remove an item from a slice. Knowing about delete, I supposed that there would be something similar for slices. But there isn't! How to delete an item from a slice?

I bet you wouldn't guess it. This is how:



    slice := []string{"a", "b", "c"}
    i := 1

    // remove item at index i
    slice = append(slice[:i], slice[i+1:]...)



This idiom is used all over the place for this purpose because there's just no way to declare a function that does it for slices of any type (that would require generics) and the Go creators decided a built-in like delete was not necessary.

But at least they provided Go programmers with the append built-in, so they can just implement that themselves.

You might have notice the ... in the code above. We'll see what that means in a second, but before we move on, there's a few more weird thins I think I should mention!

Another feature that nearly every other language has but is missing in Go is enums.

To be fair, it's not completely missing, as there is a construct called iota which is supposed to compensate for that. I'll leave the details out (check the link for details), but here's, more or less, how you handle enumerated values in Go:



type Weekday int

const (
     Sunday = iota
     Monday
     Tuesday
    Wednesday
     Thursday
     Friday
     Saturday
)



Unfortunately, although this allows you to have something that resembles proper enums, this is not type-safe in the sense that you could pass -10 or whatever to any function that accepts a variable of type Weekday.

To finalize a section on the weird bits of Go, I couldn't do better than show how time format works in Go.

In Java and every other language I know of, time formatting is done via a String template with codes like H for hour and m for month, so you end up with templates like yyyy-MM-dd HH:mm:ss, which resolve to date-times like 2017-12-28 17:21:00.

Not so in Go! Because the traditional template may be confusing to newbies (for example, is mm month or minutes?), Go came up with a novel system.

Here's how to print the current time using the same format mentioned above with Go:



    println(time.Now().Format("2006-01-02 15:04:05"))



To understand why, I will just quote the Go docs:

These are predefined layouts for use in Time.Format and Time.Parse. The reference time used in the layouts is the specific time:

Mon Jan 2 15:04:05 MST 2006

which is Unix time 1136239445. Since MST is GMT-0700, the reference time can be thought of as

01/02 03:04:05PM '06 -0700

The cool bits


The append built-in is interesting because it shows how to use variadic parameters in Go. In the case we saw previously, to spread the slice into a variadic parameter.
Luckily, it is actually possible to declare a function that takes a variadic parameter:



func myAppend(s string, ss ...string) string {
     b := bytes.NewBufferString(s)
     for _, item := range ss {
         b.WriteString(item)
     }
     return b.String()
}



This function can then be used in any of the following ways:



    strings := []string{"dear ", "world"}

    println(myAppend("hello dear world"))
    println(myAppend("hello ", "dear ", "world"))
    println(myAppend("hello ", strings...))



Which prints hello dear world 3 times.

As mentioned briefly before, unlikely Java and most other object-oriented languages, Go does not have classes. But it still allows us to define methods for structs, maintaining the benefits of strong encapsulation!


By adding methods to structs, we can also make the struct implement an interface to achieve polymorphism -
a struct implements an interface implicitly, i.e. it can be used where an interface is required as long as it implements all methods of the interface.



For example, consider the MyStruct defined earlier. Here's its definition again:



type MyStruct struct {
     enabled bool
     name string
     values []int
}



We could define a method called Sum that sums over all values of a MyStruct instance with the following code:



func (m *MyStruct) Sum() (result int) {
     for _, item := range m.values {
         result += item
     }
     return
}



Here, I intentionally introduce quite a few features not mentioned earlier. For example, notice the return value is named (called result in this case), so we don't need to declare it in the function body or in the return statement at the end.

Also notice the for loop. Go supports simple indexed loops like most other imperative languages as well:



    for i := 0; i < 10; i++ {
        println(i)
    }



But the for loop in the previous example looks quite different. That's because that's a for-each loop.
It works like this: the range keyword iterates over data structures, returning two values. In the case of slices, an index and the current item.
In the Sum function, we throw away the index (because it is not needed in this case) by declaring it with _, using only the current item.

You cannot declare variables and not use them in Go, doing that causes a compiler error! That's why we need to say we don't care about the index by naming it _. I noticed that this feature saved me from writing quite a few bugs!

One more detail in the Sum function: notice that the parameter type is prefixed with a *. That indicates the variable is pointer to something of type MyStruct, very similar to C. And as in C, to deference a pointer, you would normally use the & character. But that's only required to call functions in Go, methods can be called without that!

Hence, this is how you can call the Sum method on an existing instance of a struct:


   
n
:= MyStruct{
        values: []int{1, 2, 3, 4, 5},
    }

    fmt.Printf("Sum of n: %d\n", n.Sum())



If Sum were a function, you would need the & symbol:



    fmt.Printf("Sum of n: %d\n", Sum(&n))



It is preferable to use pointers when passing structs around because otherwise Go would make a copy of the struct when passing it to the function or method (unless that's exactly what you need, even if it might be less efficient).

Before we move on, remember how pointers can be nil? So every time we handle a pointer, we need to be careful to handle nil, so our Sum function should really be something like this instead:



func (m *MyStruct) Sum() (result int) {
     if m == nil {
         return
     }
     for _, item := range m.values {
         result += item
     }
     return
}



Now, moving on... Go has a feature to defer the execution of a method invocation until the end of the function where it is invoked, regardless of whether the function panics (for Java developers, similar to throwing an Error).

This feature is commonly used to release resources in a safe manner. For example, suppose you need to write to a file. The common idiom is to defer a call to its Close method immediately after assigning it (and checking for an error):


    
    file, err := os.Create(filePath)
    if err != nil {
        return err
    }
    defer file.Close()



There's a few things you must understand to be able to use defer correctly, but the Go Tour explains that much better than I could, so just have a look at the Tour for the details (the Tour is a really great way to learn Go, by the way!).

This code snippet shows another cool thing about Go: its error handling.

Most functions that have a good likelihood of failing, like opening a file or reading from a socket, return both a value and an error. If an error occurs, then the error result will not be nil, otherwise you can safely use the success return value.

To propagate the error, just return it from your own function. To give up on error, call panic(err), which is meant to crash the program (though there are ways to recover upstream), similar to Exceptions in languages that have them.

The possibility to return multiple values from a function is really useful, and not only to return the tuple (success, error).

For example, this is a function I wrote in the project I am working on, currently:



func removeEntryFrom(entries *[]LoginInfo, name string) ([]LoginInfo, bool) {
     if i, found := findEntryIndex(entries, name); found {
         return append((*entries)[:i], (*entries)[i+1:]...), true
     }
     return *entries, false
}



It returns the new slice with the target entry removed if it is found, along with a boolean that tells the caller whether the entry was removed.

There's quite a few other cool things about Go, but to not make this blog post even longer, I should stop here and just mention goroutines and channels, which are the primitives used to achieve concurrency in Go.

Even though I haven't had the chance to use them yet, they seem quite approachable and are definitely one of the strongest points of Go.

Again, I will defer (sorry the pun) to the Go Tour (click on the link) for a short and easy explanation of how they work.



Conclusion


Go does justice to its reputation: it is a really simple language and runs quite fast.

On the one hand, the haters seem to be right about some things. Go really misses some very basic features that most modern languages have adopted: generics, enums with powerful tricks like in Kotlin and Rust, functional programming features (no sign of map/filter/reduce in Go - no doubt due to the lack of generics), and even a built-in package manager (though I really like the simplicity of go get, and go dep seems to be finally unifying the tooling around Go dependency management).

But on the other hand, the weaknesses of Go may play to its advantage: I read a lot of Go code while working on my first project with the language, and I could understand basically all of it without trouble! The simplicity of the language is essential for that to be possible.

The Go compiler is extremely fast. My project was compiling so fast sometimes I was not sure it actually did anything :) Only after the project grew to a couple of thousand lines of code, did I notice a small, sub-second delay when compiling!

There are excellent IDEs for Go: the free VSCode plugin is very good even for someone accustomed to the power of IntelliJ.

Jetbrains has a full-blown IDE for Go, Goland but it is not free... but I found out that because I already have a IntelliJ Ultimate license from work, I could just use the Go Plugin in IntelliJ Ultimate without extra cost (unfortunately, it seems that a license is required to install this plugin... I was not able to install it at home without the license - hence why I've been using mostly VSCode). The IntelliJ plugin is definitely better than VSCode, with lots of small conveniences missing from VSCode (automatic imports, refactoring, quick-fixes etc), but I was actually surprised at how close VSCode is to the IntelliJ experience!

All in all, I can definitely understand why so many successful products are written in Go: it is a very productive language, and even though it is native, it is still very easy to compile to multiple targets.

The simplicity of the language does annoy me at times when writing yet another old-fashioned for-loop, but that simplicity seems to be its greatest feature, after all.





Comments