Writing Microservices in Go Part II – When Not to Use Go

microservices-part-2-when-not-to-use-goIn Writing Microservices in Go pt. 1, we highlighted a whole bevy of reasons for coding in Go, the novel, forward-thinking language developed at Google. However, there are two sides to every coin; the benefit of a language or system can often be a drawback in other use cases, and a host of hidden faults can make what was once an awesome idea significantly less awesome.

In this piece we take another, more critical look at Go, and identify some use cases when Go may not be the best choice for an enterprising microservice developer.

A Summary

As a refresher, the Go language was developed in 2007 as an internal project overseen by Robert Griesemer, Rob Pike, and Kem Thompson, then employees of Google. The language, referred to interchangeably as “Golang”, is loosely based on the C coding language, borrowing much of its syntax from the language.

The dependence on standard libraries and older system syntax makes Golang a purported language of simplicity, delivering a much-needed user-friendly edge. These qualities led to the language being officially entered into the public repository in 2009 and has since been adopted for a variety of projects.

Go has a lot going for it. because it was developed using an older language syntax, many users coming from a development perspective will find a simpler, but somehow more powerful, coding language that is easy to understand and implement. This legacy-positioned mindset is juxtaposed with a growing base of experimental feature-sets, making the language one of the most extensible ones available.

On paper, Go seems like the perfect language. However, behind the hype that so often permeates through the development arena, there are some serious issues with Golang that are very rarely touched upon that should cause many to take pause before adoption or application throughout the API Lifecycle.

Blog Post Tour CTA 5-01

Dependency Management

One of the great things about Go is that it has a huge library of dependencies that can be called at any time to extend the functionality of your code. However, one of the drawbacks of this source control hosting methodology in Go is the lack of versioning.

When calling any dependency, developers must consider a set of variables that make their choice of dependency clear:

  • When was the code last updated?
  • Are there any security vulnerabilities?
  • Have there been major forks for functionality that might better support my code?

All of this is well and good, but is functionally useless in Golang. You cannot select a version of source code from services such as Github or Bitbucket. Regardless of what version you want, selecting a dependency only selects the currently available one (which is usually the most recent, excluding special beta and testing versions). In other languages, the effort required to enact versioning is as simple as affixing a number to your package request — i.e. “get ModuleTestVar 1.79 Beta”. In Go, there’s no support in the base code.

This a huge issue — such a large issue that the Golang team addressed it in a community post. The solution given by the team is to simply build an internal reference file, which is fine for small projects, but adds to the complexity and size of a suite or application as the project grows. Add onto this that there is to-date no consensus on Golang file format or layout, and you quickly enter a realm of unknown.

The Go Type System and Generics

The largest problem with Go is one that is in stark contrast to the perceived high-extensibility of the language — the Golang type system and support for programming generics. While there are many approaches to API development, one of the most common is the development of generic code for mass application.

For instance, if a developer wants to create a system to handle payments, render images, and stream media (acting as a media streamer such as Spotify or iHeartRadio), a server renderer and streamer must first be developed. Instead of developing a separate media renderer and streamer for each type of media, these types can be served by a single variable renderer on the same API, resulting in a more lightweight, easier to maintain codebase.

The best solution for this (at the moment) is constrained-type generics — that is, a generic piece of code that can be applied to a variety of types set as a variable. In Haskell, an alternative code base, this can be resolved using generic functions:

id :: t -> t  
id a = a

Simply put, this code generates a generic function by the name “id”, and then generates a return based on that function. This function can be literally any type, due to the fact that Haskell preserves the type that is passed through it, along with any variables appended to the data. We can then use this data to create a function that returns modifications on that data stream:

modifyvar :: Num t => t -> t-> t -> t

modifyvar x y z = x + y + z

Notice that there was no setting of variables or verbose constraining of data — the compiler interprets this function as a simple constraint on type “t”, where “t” must be a “Num” value.

Go does not function this way. Because Golang does not have a well-functioning type system, developers have to use the interface system, which generates code that is far longer than it needs to be. Take for instance this comparison between our generic id function constraint.

In Haskell:

id :: t -> t  
id a = a

And the Golang equivalent:

type Hashable interface {  
  Hash() []byte  
}

func printHash(item Hashable) {  
   fmt.Println(item.Hash())  
} 

While it’s true that this is a valid generic type, the “value” type of the “interface” variable is a “top type”, meaning that all variations on that type are actually a subtype of the “interface” variable. While Golang technically does not support subtyping, in practice this holds true because the variables modified from “interface” depend directly on the first definition of “interface” — meaning that the top type must be monolithic and include all possible variables. In other words, the “variable” in Golang is actually a static, non-learning set, whereas in other languages, such as Haskell, learning is intrinsic to the language and the handling of generic types.

Furthermore, this breaks down-cast from the top type, meaning that any derivative variables will not properly compile, crashing your API as soon as the code tries to down-cast a variable not explicitly stated or included in the top-type. Simply put, there is no dynamic variable type in Golang, which makes development of “Swiss Army Knife” style API frameworks completely impossible without hacking the language far beyond what is possible in the default implementation of other languages. Functionality is important, but it must be balanced with usefulness and approachability.

Extensible – But Not Really

Interestingly, one of the big selling points of Golang is in turn one of its biggest negatives — extensibility. While it’s true that Go is very extensible, and through a variety of frameworks can be adapted into a gigantic host of variable applications, the type issue from earlier rears its ugly head over and over, making this extensibility come at a price — complexity. And, seeing as how simplicity is a selling point of Golang, adding complexity seems contradictory.

Take for example a very simple math problem: a(b) + 5 * b(c). To render that out in Golang, you need to define each variable as a modifier of the other. In other words, your code ends up looking like this:

a.Mul (b) .Add(big.NewInt (5) .Mul(b) .Mul(c))

To do this in Haskell, you simply create the following line:

a*b - 5 * b * c

As the complexity of an API expands, it’s important to remember that it’s not just the user experience that matters, but the developer experience as well — and as a developer, would you rather see example A or example B? Go is very powerful, but with such massive expansion of otherwise simple functions, what might take 50 or 60 lines in Haskell quickly ends up taking 200 or 250 lines to handle. Allowing for manipulation of data is so vitally important that making it this difficult is simply a poor approach.

*Since publishing this piece, multiple readers have pointed out a perceived flaw in the math problem presented. The above problem as written is specifically a BigInt problem, and not an int64 problem.

Alternatives

It’s fine to state something is bad, but providing solutions — now that’s important. Luckily, there are a few things a developer can do to get away from Go and its native issues.

Modify Go

Development languages benefit from the fact that, if there’s a problem, there’s likely a solution. Go can be modified by any number of frameworks to make it function in the way we want it to, and many of the biggest issues inherent in the Golang language are solved by these frameworks:

  • Revel: Allows for wildcards in URLs, and matched parameters using “map[string]string”, which allows for more complex inherited data types and values through reflection and injection.
  • Gorilla: Provides for a more robust and powerful routing methodology than the base code of Go. Also allows for much of the same injection-based “id” inheritance through “map[string]string” as Revel, but does not require exterior dependency to do so.
  • Gocraft/web: Supports both URL wildcards and URL regular expressions (regex), such as “/:id:[0–9]+”. Utilizes routes in a tree rather than a list, and supports the “map[string]string” functionality mentioned above through native implementation of the “http.ResponseWriter” and “http.Request” calls and “web.Request” type formatting.

The problem with modifying Go, however, is that you are functionally modifying a system rather than utilizing a base application. This means code bloat, complexity, and difficulty with maintaining not only your own code, but the derivative systems used to make the code work. This added complexity begs the question: if you’re using Go for simplicity, and it results in added complexity, why are you using Go?

Other Languages

There is of course the option to move away from Go. Several languages have been designed alongside Go or as an alternative to Go, specifically created or modified to resolve issues inherent in both Go and other languages such as C or Java while allowing for the use of a diverse range of API architectures and approaches.

Conclusion

Unfortunately, the conclusion of this piece is likely one that you’ve heard before — “it depends”. API development is such a large topic that no one piece is going to convince you to adopt a language that you are unfamiliar with, or abandon a language you desperately love.

The issue comes down to this — can you live without versioning? Can your code function without generic types? Are you willing to implement a framework over a language in order to make it function at the same level other languages do out of the box? Because Go increases in complexity as API requirements increase, designing a generic API with multiple functions and applications becomes far more difficult than it ever should be. Pair this with the addition of lacking versioning support and code bloat, and you may find that using Golang becomes a daunting proposition.

But what if you can live without versioning? What if you don’t need generic types? What if you’re comfortable with additional frameworks and a quick fix or two? When developing simple, single or low function Lean Strategy-based APIs, Golang is a wonderful choice, and can cut down the complexity of development dramatically when compared to other similarly verbose languages.

In fact, when developing a simple API, those weaknesses often become strengths — versioning becomes less an issue in an API utilizing few dependencies, generic API functions aren’t important for specific tasks, and code bloat is a minimal problem with a small base code.

Share

Using Go for your project? Have an opinion either way? Feel free to share your thoughts or experiences below, or reach out to us on Twitter.