We recently wrote an API in Go, an open-sourced programming language that came out of Google in 2009. We learned a lot along the way, and we thought we would share.
When choosing a programming language for a project, we always recommend understanding what you’re trying to build before even thinking about what language to build it in. Let the product be the determining factor in how it should be built.
Below are some pros and cons of working in Go to help you know if Go is right for your next project.
The Go programming language has seen explosive growth in usage in recent years. It seems like every startup is using it for their backend systems. There are many reasons that developers find it attractive.
Go is a really fast language. Because Go is compiled to machine code, it will naturally outperform languages that are interpreted or have virtual runtimes. Go programs also compile extremely fast, and the resulting binary is very small. Our API compiles in seconds and produces an executable file that is 11.5 MB. You can’t complain about that.
Go’s syntax is small compared to other languages, and it’s easy to learn. You can fit most of it in your head, which means you don’t need to spend a lot of time looking things up. It’s also very clean and easy-to-read. Non-Go programmers, especially those used to a C-style syntax, can read a Go program and usually understand what’s going on.
Go is a strongly, statically typed language. There are primitive types like int, byte, and string. There are also structs. Like any strongly typed language, the type system allows the compiler helps catch entire classes of bugs. Go also has built-in types for lists and maps, and they are easy to use.
Go has interfaces, and any struct can satisfy an interface simply by implementing its methods. This allows you to decouple the dependencies in your code. You can then mock your dependencies in tests. By using interfaces, you can write more modular, testable code. Go also has first-class functions, which open up the possibility to write your code in a more functional style.
Go has a nice standard library. It provides handy built-in functions for working with primitive types. There are packages that make it easy to stand up a web server, handle I/O, work with cryptography, and manipulate raw bytes. JSON serialization and deserialization provided by the standard library is trivial. With the use of “tags”, you can specify JSON field names right next to struct fields.
Testing support is built into the standard library. There is no need for an extra dependency. If you have a file called thing.go, write your tests in another file called thing_test.go, and run “go test”. Go will execute these tests fast.
Go static analysis tools are numerous and robust. One in particular is gofmt, which formats your code according to Go’s suggested style. This can normalize a lot of opinions on a project and free your team to focus on what the code is doing. We run gofmt, golint, and vet on every build, and if any warnings are found, the build fails.
Memory management in Go was intentionally made easier than in C and C++. Dynamically allocated objects are garbage collected. Go makes using pointers much safer because it doesn’t allow pointer arithmetic. It also gives you the option of using value types.
While concurrent programming is never easy, Go makes it easier than in other languages. It is almost trivial to create a lightweight thread, called a “goroutine”, and communicate with it via a “channel.” More complex patterns are possible.
As we discussed above, Go is a good language. It has a clean syntax. It has fast execution times. There are plenty of good things about it. However, a programming language is more than its syntax. Here are some things that we ran into.
First, the elephant in the room. Go doesn’t have generics. This is a big hurdle to get over when coming from languages like Java. It means a decreased level of reuse in your code. While Go has first-class functions, if you write functions like “map”, “reduce”, or “filter” that operates on a collection of one type, you can’t reuse those same functions for a collection of a different type. There are ways to deal with this, but they all ultimately involve writing more code. This hurts productivity and maintainability.
While having interfaces is great, structs implement interfaces implicitly, not explicitly. This is stated as a strength of Go, but we found that it’s difficult to tell from looking at a struct whether or not it implements an interface. You can only really know by attempting to compile the program. This is fine if the program is small, but not if it’s a medium to large size.
The Go community can be non-receptive to suggestions. Consider this issue in the GitHub repository for golint: https://github.com/golang/lint/issues/65. Someone requested the ability for golint to fail a build when there are warnings found (which is something we’re doing on our project!). The maintainer immediately dismissed the idea. Enough people commented on the issue, and the maintainer eventually added the requested feature, over a year later.
The Go community also appears to have an aversion to web frameworks. While Go’s HTTP library covers a lot, it doesn’t provide support for path parameters, input sanitization and validation, or many cross-cutting concerns often found in a web application. Ruby developers have Rails, Java developers have Spring MVC, and Python developers have Django. But many Go developers choose to avoid frameworks. Yet there are still frameworks out there. A lot of them! But it’s nearly impossible to choose one that won’t be abandoned after you’ve started a project with it.
For a long time Go did not have a stable, official package manager. After years of begging from the community, the Go project has recently released godep. Before that, many tools filled the gap. We use the very capable govendor in our project. But this means the community is fractured, and it can be extremely confusing for developers new to Go. Also, virtually all package management is backed by Git repositories, the history of which could change at any time. Compare this to something like Maven Central, which will never delete or change a library that your project depends on.
Sometimes, you need to think about the machine. You send and receive bytes. You manage thousands of concurrent threads. You might be writing an operating system, or a container system, or a blockchain node. In these situations, there’s a good chance you don’t care about generics. You’re too busy squeezing every nanosecond of performance out of the silicon.
Often times, though, you need to think about the squishy, carbon-based humans. You need to work with business domain data: customers, employees, products, orders. You need to write business logic that operates on collections of these domain entities, and you need to maintain this business logic for years. You need to deal with shifting requirements, and you need to do it yesterday. For these situations, the developer experience matters.
Go is a programming language that values machine time over human time. Sometimes your domain is the machine, or the performance of your program is critical. In these situations, Go could make for a good C or C++ alternative. When you’re writing a typical n-tier application, though, performance bottlenecks will usually appear in the database and, more importantly, how you model your data.
Consider this rule of thumb when deciding whether to use Go:
If you’re working with bytes, Go might be a good choice.
If you’re working with data, Go might not be a good choice.
Maybe someday this will change. The Go language and community are still relatively young. They could surprise us and add generics, or a popular web framework could emerge as the clear winner. For now, though, we’ll stick with proven languages that have ubiquitous support, mature dependency management, and a focus on modeling business domains.