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
4 thoughts on “To Go or not to Go”
This is actually a very insightful and well written post. I enjoyed reading it and learning about Go, especially since I never worked with it before. Thank you Alexander
How did you make it to write docs day and night? Lol writing docs during work hours is already enough to me
Spot on with this write-up, I truly feel this web site needs a lot more attention. I’ll probably be returning to read more, thanks for the information!
Wow, that’s what I was searching for, what a material!
present here at this webpage, thanks admin of this site.