Why I like Go so much, with examples

Matt Simmons
Datasparq Technology
8 min readMar 17, 2022

Out of all the languages I’ve used, Go is the best. I find myself saying this to a lot of people, so here’s my attempt to explain why.

My perspective comes from years of data engineering with Python and front-end development with JavaScript/TypeScript. I’ve been exposed to a fair amount of C, C++, Java, C#, and R, and have formed a lot of opinions about the languages themselves, as well as their implementations and ecosystems.

It’s Clean

Here’s the ‘hello world’ program for Go. It’s like many others:

package main

import "fmt"

func main() {
fmt.Println("Hello, world!")
}

There’s no extra fluff like void , int main(), or return 0;. Instead, these are implied by the lack of arguments or return type in the function definition. Someone with no knowledge of the Go language can easily understand what’s going on.

There are no globally defined functions used; every function comes from a named package. This makes it vastly easier to follow than C++ or Java.

Public functions must start with capital letters, which avoids the need for public or private everywhere, and allows me to see which functions are public at a glance, and without seeing the definition.

Here’s a loop over a slice (a list):

for index, item := range myList {
fmt.Print(item)
}

Here’s a while loop. It’s still a for loop but it uses a condition rather than a range:

sum := 0
for sum < 1000 {
sum += sum
}

There are little syntactic shortcuts like this everywhere.

It’s Fast

Surprisingly fast.

It’s Forgiving

I mentioned it’s fast, but for someone like me who can’t write C very well, it’s faster than any other language. This is because it optimises things for you. Forgot to make something a pointer? No problem!

Slices

Slices make arrays easier by abstracting away management of array length. They remain very fast but are also forgiving, providing a perfect middle ground between a Python list and a C++ array. Fixed length arrays are also available if I need them.

var s = []int{1, 2, 3}s = append(s, 4)  // slice grows in sizefmt.Println(s)  // [1 2 3 4]

Goroutines

Goroutines are lightweight threads. They make parallelising code very easy and the syntax is much cleaner than most other languages, many of which have added concurrency as an afterthought.

They are as simple as they could possibly be. Just define any function as normal and use the go keyword.

go listenForPings()

Channels

Sometimes you need to send information to a goroutine after you’ve started it, or get something to wait for a message before running. Channels do all of that and more.

I can make a channel that sends strings with ch := make(chan string). Then I can pass it to a function with go x.monitor(ch). My function (now running in a goroutine) will send messages to this channel (note the <- syntax ).

func (x* HeartBeatDetector) monitor(channel chan string) {  
if time.Since(x.Last) > x.Rate {
// send name to channel to report dead process
channel <- x.Name
}
...
}

Other functions could read the messages from this channel. This for loop will keep going until the channel is closed with close(ch):

for d := range ch {
fmt.Printf("Oh dear, %v died!\n", d)
}

Error Handling

No more try/catch blocks. Now errors are returned by any function that could result in an error:

err, result := myFunc()
if err != nil {
panic(err)
}

This may look ugly to a Python user, as there are now three extra lines of code for what is essentially an unhandled error, but at least now I (and my IDE) can see exactly what’s going to happen if there’s an error, so I won’t miss anything.

Here’s some more precise error handling, where I check the type of the error:

if err := Foo(); err != nil {
switch e := err.(type) {
case *SyntaxError:
// do something interesting with e.Line and e.Col
case *InternalError:
// abort and file an issue
default:
log.Println(e)
}
}

Static Types

In my experience, typing ultimately makes code easier to write and understand. The downside is that it can lead to a bulky codebase. In Go, things are fairly clean.

I can define a type like this:

type item struct {
id int
value string
}

Then define a method for my type:

func (i *item) String() string {
return i.value
}

Then define a function that uses my type:

func ConcatItems(a item, d item) {
return a.String() + b.String()
}

I also need a method to create new instances of my type. The convention is to use the following pattern, where a function New creates the type defined within that module:

func New(id int, value string) item {  
i := item{id, value}
return i
}

JSON

I can define how a type should be represented in JSON, then read a JSON string and convert it directly to my type. Validation is automatic.

type Mission struct {
Name string `json:"name"`
Services []Service `json:"services"`
Stages []Stage `json:"stages"`
Params map[string]string `json:"params"`
isValid bool
}
func NewFromJSON(jsonString []byte) Mission {
var m Mission
json.Unmarshal(jsonString, &m)
return m
}

Easy to Compile

I mean this both in general (simple command: go build, helpful error messages, fast compile times, easy install in the first place) and in terms of all the different operating systems and architectures I can compile to, including Web Assembly, which has allowed me to run my Go package natively in the browser!

I’ve used a simple build pipeline on GitHub to build my packages for multiple architectures. It compiles binary distributions for 5 different platforms, and I didn’t have to configure anything.

Runes

Runes represent unicode codepoints, which get stored as int32. This means I can treat a and in exactly the same way, even though a has one byte and has 3 bytes. Thanks to runes, I can confidently parse text containing non-ASCII characters without messing anything up.

Go’s syntax treats 's' as a rune, and "s" as a string of length 1. 'hello' would be invalid because it has multiple characters and therefore can’t be a rune. It only took a few minutes to get used to this, and it’s really nice to work with.

go get

I’ve gone through the process of turning Python code into packages and uploading to PyPi a few times now. It’s still confusing, and takes me a while.

With Go there’s no confusion. The command is 5 letters. The package location is the repo name, the install location is the repo name, the import path is the repo name. Very simple.

go get github.com/MattSimmons1/bb

It also compiles and installs the package automatically. I appreciate this as it allows people to install my program with a single line.

Built-In HTTP Server

Admittedly, I would rather use Python’s Flask or FastAPI for most use cases, but Go’s http server is very light weight. I’ve used it countless times.

http.HandleFunc("/", serveHome)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}

Requests

The http module does what you’d expect. It’s nice that I don’t need to install any additional modules.

postBody, _ := json.Marshal(map[string]string{
"foo": "bar",
})
resp := http.Post("localhost:5000/api",
"application/json", bytes.NewBuffer(postBody))
defer resp.Body.Close()

body := ioutil.ReadAll(resp.Body)

var res []Response
json.Unmarshal(body, &res)

If you need to do anything over UDP, TCP, DNS, or Unix Sockets, you can use net. Here I’m sending a string over UDP in the same way I would send a string to a file.

conn, err := net.Dial(“udp”, “127.0.0.1:49161”)fmt.Fprintf(conn, "03C;13C;24C")  // send data

Cobra

Cobra is a library that can turn a go project into an easy to use command line tool in minutes. It’s used like this:

func main() {

if err := func() (rootCmd *cobra.Command) {

rootCmd = &cobra.Command{
Use: "my-cli",
Short: "My Tool's Command Line Interface",
Args: cobra.MinimumNArgs(1),
Run: func(c *cobra.Command, args []string) {
DoStuff(args[0]) // my function
return
},
}
rootCmd.AddCommand(func() (createCmd *cobra.Command) {
createCmd = &cobra.Command{
Use: "version",
Short: "Print the version number",
Run: func(c *cobra.Command, args []string) {
fmt.Println("v0.1.0")
},
}
return
}())
return
}().Execute(); err != nil {
log.Panicln(err)
}
}

I could then run my-cli hello to run the main program, my-cli version to run a subcommand, or my-cli help (a subcommand Cobra made for me).

If I were to just run my-cli with no arguments, I would see:

Error: requires at least 1 arg(s), only received 0
Usage:
my-cli [flags]
my-cli [command]
Available Commands:
help Help about any command
version Print the version number

I can easily add all of the flags and subcommands I need, and don’t need to worry about overcomplicating my code. Many projects use Cobra, including Kubernetes, so I can be confident that the interface for my tool will be familiar to lots of developers.

time

Go comes with the two types time.Time and time.Duration. Both give you nanosecond precision and are really easy to work with.

Use time.Now() to get the current time. If you want to know the duration since that time, use d := time.Since(t1), then format that duration however you like, or just print it’s value and it will automatically be formatted in a sensible way.

Unlike Python, I don’t have to convert between datetime.date, datetime.datetime, datetime.time, pandas.Timestamp, and time.struct_time. There’s just one time type that does everything I need it to.

Mutex

The mutex library allows me to easily guarantee that my Goroutines won’t try to update the same value at the same time. This isn’t relevant without regular use of Goroutines, but it’s very powerful.

Here I’ve given my type a property called mux of type sync.Mutex for making sure that multiple go routines can read and write to last, which records the last time a heartbeat was detected.

func (x *HeartbeatDetector) detected() {
defer x.mux.Unlock()
fmt.Printf("❤️")
x.mux.Lock()
x.last = time.Now()
}

I also used the defer keyword here to make sure the mutex always unlocks when the function ends.

Mascot

Go’s mascot perfectly represents the philosophy behind the language.

It’s simple. It has everything it needs and nothing extra. There’s only a few basic features shown, but you can understand what it is right away.

I can’t think of a better mascot in the world of software.

Developer Experience

If you’re using a good IDE (I strongly recommend GoLand), it will point out every potential issue to you as you code, so you’ll practically never have any unexpected errors. It shows the types of every variable and every unhandled error.

The most helpful thing it does is automatically add every module I reference to my import statement at the top of the file. If it weren’t for this, I would be wasting time trying to remember the location of every module, or just forget to import and get errors when I try to run.

Tutorial

gobyexample.com provides the perfect way to quickly learn a new language. It starts simple, goes through each concept one at a time, and lets you run code in your browser.

Conclusion

In conclusion, Go is amazing. It should be part of everybody’s arsenal, along with Python (for simple scripting/data wrangling) and JS (for front-end). Go is perfect for anything that needs to be reliable and high performance, such as a Microservice, developer tool like Kubernetes or Terraform, or Data Science tool.

If you’d like any more tips you can find me on Twitter:
@MattSimmons01 & @DatasparqAI.

Reach out to us at datasparq.ai/contact for advice on solving a problem with Data Science!

--

--