Why I like Go so much, with examples
--
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!