Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
This is an old question that I still get asked by a lot of developers. First of all, let me just say that yes, I know that eventually Go will have Generics and that there is already a big discussion and even a draft on the subject at the moment that you can read it over here if you are looking for some light reading.
To begin, let's refer to the golang.org FAQ on the subject.
Generics are convenient but they come at a cost in complexity in the type system and run-time. We havenât yet found a design that gives value proportionate to the complexity, although we continue to think about it. Meanwhile, Goâs built-in maps and slices, plus the ability to use the empty interface to construct containers (with explicit unboxing) mean in many cases it is possible to write code that does what generics would enable, if less smoothly.
So in this post, I want to talk about this part, how can we write code with Go that does the same or at least enabled what Generics would, even if less elegantly.
So, What are Generics?
The problem
Many data structures and algorithms are applicable to a range of different data types. A sorted tree, for example, could be defined to hold elements of type int, or map[string]string, or some struct type. A sorting algorithm is able to sort any of those types as long as they are comparable to each other. So if you need a sorted tree of strings, for example, you could easily sit down and write one.
But what happens if you need a sorted tree of many different types? And each tree datatype comes with a couple of methods attached to it, like Insert, Update and Delete. If you have N tree elements, with M tree methods, to implement the tree you would need N x M methods! Doable, but very repetitiveâââgood luck with that!
Generics to the rescue
To address this problem, many programming languages have the concept of Generics. The point is to write code only once, in an abstract form, with generic types placeholders instead of real types. Here it is an example, written in Java:
// Java pseudo codepublic class SortedTree<E implements Comparable<E>> {void insert(E comparableSortTreeElement) {//... }}
E is a type placeholder; it can appear in class or interface definitions, as well as in function parameter lists. SortedTree is used by substituting a real type for the placeholder, like this:
// Java pseudo codeSortedTree<String> sortedTreeOfStrings = new SortedTree<String>();sortedTreeOfStrings.insert("abcde");
These lines instantiate a sorted tree with string elements. The insert method then only accepts strings, and the sort algorithm uses string comparison methods. Had we used Integer instead, like in SortedTree<Integer>, the tree would be a sorted tree of integers instead. Works like magic!
To summarise, generics allow us to define classes and functions with type parameters. The type parameters can be restricted to a certain subset (e.g., parameter T must be comparable, don't worry about this Java-specific stuff), but otherwise they are unspecific. The result is a code template that can be applied to different types as the need arises.
Are there any downsides?
Yes, it is not all roses. Although Generics may come in handy, they also have some downsides.
1. Perfomance: Turning a generic code template into actual code takes time, either at compile time or at runtime. As stated by Russ Cox in 2009:
The generic dilemma is this: do you want slow programmers, slow compilers and bloated binaries, or slow execution times?
2. Complexity: Generics are not necessarily complex, but they can be when integrated with other languages features such as inheritance, or nested generics, like:
// C#List<dictionary<string,IEnumerable<HttpRequest>>>
Or even recursive inheritance like:
// Javapublic abstract class Enum<E extends Enum<E>>
(taken from this Java Generics FAQ by Angelika Langer).
Go has no Generics. What now?
As we are all aware Go has no Generics, it was designed with simplicity in mind and Generics as mentioned above is considered to add complexity to the language. The same goes for Inheritance, Polymorphism and some other features that the top object-oriented languages showed when Go was created.
All is not lost!
Go does have some Generics, like features
There are a few Generic constructs in Go. Firstly, there are three generic data types that you can make use of, and probably already have:
- arrays
- slices
- maps
All of these can be instantiated on arbitrary element types. For the map type, this is even true for both the key and the value. This makes maps quite versatile. For example, map[string]struct{} can be used as a Set type where every element is unique.
Second, some built-in functions operate on a range of data types, which makes them almost act like generic functions. For example, len() works with strings, arrays, slices, and maps.
I know this may not be exactly what you were looking for, but chances are that there built-in Generics that already cover most of your needs.
But what if it does not, and you really need some Generics?
There are times that you can come across a problem that Generics just might be the perfect fit, if not indispensable. Luckily there are a couple of things you can do.
1. Review the requirements
Do the specs really demand Generics? Consider that although other languages may support a design that is based on type systems, Go is a bit different.
If C++ and Java are about type hierarchies and the taxonomy of types, Go is about composition. (Rob Pike)
So stop trying to do things in Go the same way you would do things in other languages. Do things the Go Way.
Think of the paradigms that come with Go, most notably composition and embedding, and verify if any of these would help to approach the problem in a way more natural to Go.
2. Copy &Â Paste
I know this may sound foolish and it goes against everything you believe in, like the "keep it dry (don't repeat yourself)" principle, and it can be if applied improperly, but be so quick to dismiss it completely.
And hereâs why.
Every time you think that you need to create a generic object, do a quick Litmus test: Ask yourself, âHow many times would I ever have to instantiate this generic object in my application or library?â In other words, is it worth to construct a generic object when there may only be one or two actual implementations of it? In this case, creating a generic object would just be an over-abstraction.
If you want a real-life example we can look into the standard library. The packages strings and bytes have almost identical API's, yet no generics were used when creating these packages.
3. Interfaces
Interfaces define behaviour without requiring any implementation details. This is ideal for defining âgenericâ behaviour, when type doesn't matter much:
- Find a set of basic operations that your generic algorithm or data container can use to process the data.
- Define an interface containing these operations.
- To âinstantiateâ your generic entity on a given data type, implement the interface methods for that type.
The sort package is an example of this technique. It contains a sort interface (called sort.Interface) that declares three methods: Len(), Less(), and Swap().
type Interface interface {Len() intLess(i, j int) boolSwap(i, j int)}
By just implementing these three methods for a data container (for example, a slice of structs), sort.Sort() can be applied to any kind of data, as long as the data has some definition of âless thanâ.
A nice aspect here is that the code of sort.Sort() does not know anything about the data it sorts, and actually, it does not have to. It simply relies on the three interface methods Len, Less, and Swap.
You can find a couple of examples in the sort package doc, but here it is one simple example that shows a collection working with multiple types, just by implementing an interface with a certain behaviour.
type Integer16 int16 type Integer32 int32
type Calculator interface { DoSomething() }
func (i Integer16) DoSomething() { /* implementation here */ } func (i Integer32) DoSomething() { /* implementation here */ }
func main() { container := []Calculator{Integer16(1),Integer32(2)}
fmt.Println(container) }
4. Type Assertions
Let's say you want to have a Generic container that does not care much about the actual type of their contents. So the value can be stored in the container as a âtype without propertiesâ. For that we could the empty interface, declared as interface{}. This interface has no particular behaviour, hence objects with any behaviour satisfy this interface. But remember
"empty {}interface says nothing", Rob Pike
Although this approach can be useful, it brings some overhead, like extra code and it does not provide compile-time checks, so as Rob Pike says himself, you might as well be programming in Python (https://www.youtube.com/watch?v=PAAkCSZUG1c&t=7m36s).
It is quite easy to build a container type based on interface{}. Like I mentioned, we need some extra code to recover the actual data type when reading elements from the container. And that is when type assertions come in. Here is an example that implements a generic container object.
Container is a generic container, accepting anything.
type Container []interface{}
Put adds an element to the container.
func (c *Container) Put(elem interface{}) {*c = append(*c, elem)}
Get gets an element from the container.
func (c *Container) Get() interface{} { elem := (*c)[0]*c = (*c)[1:]return elem}
The calling code does the type assertion when retrieving an element.
func assertExample() { intContainer := &Container{} intContainer.Put(7) intContainer.Put(42) elem, ok := intContainer.Get().(int) // assert that the actual type is int if !ok { fmt.Println("Unable to read an int from intContainer") } fmt.Printf("assertExample: %d (%T)\n", elem, elem)}
This looks easy, right?! Yeah, but as I mentioned above, let's not forget the cons of this approach. We give up compile-time type checking here, exposing the application to increased risk of type-related failure at runtime. Also, conversions to and from interfaces come with a cost. And finally, all type assertions thing happens outside our Container type, at the callerâs level. In an ideal world, you would want a technique that hides the type conversion mechanisms from the caller.
There are still some other ways you can work around Generics in Go, like using reflection for example, but I decided to not talk about that because it brings more headaches then solutions in my opinion like adds a lot of complexity, there is also no compile-time checking and adds considerable runtime overhead.
So I'll leave you guys with these ones and hope it was useful.
Like the post? How about a CLAP?!
Why Go doesn't have Generics was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.