Why Golang is great for network services

Posted on Oct 16, 2016

I’ve been using Go professionally for over a year now. While I’m not a fan of every decision made by the language, I will admit that Go gets a lot of things right for one segment of the industry - network daemons and similar systems software. Google’s ethos are engineering solid and reliable services at scale; various design decisions make Go well suited for this.

Go favors composition at multiple levels. It is the only way to re-use code at the type system. The standard library and idioms also make composition the way to plug disparate libraries and disparate services together. Goroutines and channels are a way to compose concurrency. Since service-oriented architectures compose services, this pervasive composition often maps constructs well across abstractions.

Go doesn’t offer a lot over C. But what it offers is really well suited for network services. These are almost always constrained by network/disk I/O, so GC is usually acceptable. Arrays and hashes are enough for most record keeping. Message passing and lightweight threads prevent common concurrency issues 1. That is about all the language offers.

Where Go wins is a really well designed standard library for common tasks that network services need to do.

The bread and butter of I/O composition is io.Reader, io.Writer and net.Conn. I don’t know if it was intentional, but it is as if the Go authors decided that there may be more than one way to process I/O, but only one way to combine I/O.

They clearly spent time reducing the opportunities for developers to disagree, which seems to me like a very wise move. – Go, Swift, and Corporate Culture

io.Reader and io.Writer allow composable streaming of data. net.Conn allows injecting custom network connections into your stack. This has led to wider community packages also providing the same. Most Go networking libraries will accept a custom Dial or equivalent function. Similarly various processors will read from io.Readers and write to io.Writers. In addition, common contiguous data like byte slices and strings can be seamlessly converted to readers and writers.

This makes it trivial to separate I/O and network protocol parsing. Cory Benfield highlights the problems in the Python community in Building protocol libraries the right way. The result is that Go can have high quality parsing libraries that are easy to plug into other libraries. Composition isn’t only at the interface level, but at the library level.

In addition, large swaths of the ecosystem can benefit from improvements with zero effort. For example, when the HTTP package supported HTTP/2 out of the box starting with Go 1.6, you just had to recompile the program and HTTP/2 would just work regardless of which web framework the program was using.

Good I/O primitives allow comprehensive, reproducible tests; key to shipping reliable software. It is easy to wrap various readers and writers to implement just the parts of various protocols that you need. It is easy to insert faults and ensure your program responds to them correctly. Combined with out of the box code coverage, it is relatively low effort to create comprehensive test cases.

For example, this mgo driver issue was affecting Iron.io services in production. There was a suggested fix, but no way to know if it worked. With a combination of creating a custom wrapper around a TCP socket and injecting that into the Dial function for mgo, I was able to control responses to and from the Mongo server. Then it was as easy as causing Read()s to occasionally timeout, and observing writes to see if that kicked of authenticate calls. Applying the fix solved it and was a good sanity check that the fix wouldn’t cause more problems. Here is the full test case.

Some useful I/O testing wrappers are included in the standard library iotest package.

Good I/O primitives also allow easy fuzzing of libraries. Tools like the bundled testing/quick and go-fuzz are very useful to find state machine bugs and automatically generate comprehensive test cases.

Moving away from I/O, Go also makes it easy to handle failures and resources in service oriented architectures.

The Context pattern allows simple timeouts and propagates cancellations to prevent resource consumption when the resource is no longer required. It also allows a minimal form of request tracing via the key-value store.

Combining contexts with goroutines and channels allows connection pooling and resiliency patterns to prevent cascading failures and support graceful degradation. It also facilitates easy fan-in/fan-out, aggregation, load-balancing and asynchronous routines that perform some kind of resource cleanup or cache updates. While not as powerful or elegant as promise based implementations, they get close without adding generics.

The Go standard library also ships quality implementations of things that are hard to get right but commonly used, such as various compression, hashing and crypto primitives.

The benefits of taking care of random boiler-plate cannot be understated. First class struct annotation support makes it easy to load configuration and parse requests into types. JSON and XML are supported out of the box and enough for 90% of use-cases. It is easy for third-party libraries to hook into this too. The flag package does a good job of argument parsing. Multipart-encoding is supported (as somebody who wrote a parser for it in C++, I approve). The HTTP implementation has sane connection pooling out of the box.

Go is built around long running programs and ships tools for race condition detection and profiling since the beginning. Things that need to be introspectable, are usually easy to introspect. expvar provides pull based statistics. It is easy to extract memory and GC information from the runtime.

In conclusion, Go is heavily biased towards making it easy to write reliable and performant services. It makes operating these services easier by creating a culture of making the services testable and observable. This culture was created by reducing the friction of adding those qualities to programs, and by encoding good patterns in the standard library and the documentation around the language from the very beginning. Google has created a language that works for their needs. Since other services usually operate at lower requirements, Go works really well for the rest of us too.

  1. Each of my claims here is invalidated at various scales and niche requirements; my point is, for >90% of use cases, throwing more hardware or faster disks is the right answer. ↩︎