There's been a lot of criticism about Go recently because it doesn't have support for generics. What exactly does that mean? How would you explain that to someone coming from a dynamically typed language like Ruby where this isn't a familiar concept?
With the release of generic programming in Go, we can now write a min() function that works for both integer and floating-point types without having to explicitly write them based on the types. The Go generics design basically entails allowing types and function declarations to have optional type parameters.
One key characteristics of Go generics implementation is they only partially use monomorphization, a technique used in languages like C++, D, or Rust to compile generic code. In a nutshell, monomorphization consists in replicating a function's implementation to specialize it for distinct types.
In other words, interface types in Go are a form of generic programming. They let us capture the common aspects of different types and express them as methods. We can then write functions that use those interface types, and those functions will work for any type that implements those methods.
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.
Update Q1 2022
Generics are officially supported with Go 1.18
Go 1.18 includes an implementation of generic features as described by the Type Parameters Proposal.
This includes major - but fully backward-compatible - changes to the language.
See nwillc/genfuncs
for (lots of) examples:
// Keys returns a slice of all the keys in the map.
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, len(m))
var i int
for k, _ := range m {
keys[i] = k
i++
}
return keys
}
2014:
William B. Yager blog post reminds why the "generic" part present in Go is not enough:
You can write generic functions easily enough.
Let's say you wanted to write a function that printed a hash code for objects that could be hashed. You can define an interface that allows you to do this with static type safety guarantees, like this:
type Hashable interface {
Hash() []byte
}
func printHash(item Hashable) {
fmt.Println(item.Hash())
}
Now, you can supply any
Hashable
object toprintHash
, and you also get static type checking. This is good.
What if you wanted to write a generic data structure?
Let's write a simple Linked List. The idiomatic way to write a generic data structure in Go is:
(here is just the start)
type LinkedList struct {
value interface{}
next *LinkedList
}
func (oldNode *LinkedList) prepend(value interface{}) *LinkedList {
return &LinkedList{value, oldNode}
}
The "correct" way to build generic data structures in Go is to cast things to the top type and then put them in the data structure. This is how Java used to work, circa 2004. Then people realized that this completely defeated the purpose of type systems.
When you have data structures like this, you completely eliminate any of the benefits that a type system provides. For example, this is perfectly valid code:
node := tail(5).prepend("Hello").prepend([]byte{1,2,3,4})
So that is why, if you want to retain the benefit of type system, you have to use some code generation, to generate the boileplate code for your specific type.
The gen
project is an example of that approach:
gen
generates code for your types, at development time, using the command line.gen
is not an import; the generated source becomes part of your project and takes no external dependencies.
Update June 2017: Dave Cheney detailed what Generics for Go would mean in his articles "Simplicity Debt" and "Simplicity Debt Redux".
Since Go 2.0 is now actively discussed at the core team level, Dave points out what Generics involve, and that is:
io.Reader.Read
?Iterable
interface return a value, a value and an error, or perhaps you go down the option type route.const
is insufficient, because while it restricts the receiver from mutating the value, it does not prohibit the caller from doing so, which is the majority of the data races I see in Go programs today.As Russ Cox writes in "My Go Resolutions for 2017":
Today, there are newer attempts to learn from as well, including Dart, Midori, Rust, and Swift.
The latest discussion is Go issue 15292: it also references "Summary of Go Generics Discussions".
In a dynamically typed language, you don't care what type of list it is, just that it's a list. However, in a statically typed language, you do care what type of list it is because the type is "a list of A" where "A" is some type. That is, a list A
is a different type from list B
.
So when you speak of generics, calling some function of type A -> B
each item of a list with a foreach
means that the list must be a list A
. But... if you use generics, then you don't have to declare what A
is, you can just have it be filled in at a later date. Thus, you establish the contract whereby given a list C
and a function A -> B
, A === C
in order for it to compile. This reduces boilerplate considerably.
In Go, given the lack of generics and the ability to declare such a type contract, you have to write a function that operates on a list of int, a list of double, a list of string, etc. You can't just define things in a "generic" manner.
You don't have to be left wondering now. Generics are a reality in Go. The draft release notes of Go 1.18 officially announce the introduction of type parameters into the language in a backward-compatible way.
The current set of specifications for type parameters can be found in the type parameters proposal authored by Ian Lance Taylor and Robert Griesemer.
These specs are already implemented into the language at tip, which you can run right now on the Gotip Playground.
(source)
- Functions can have an additional type parameter list that uses square brackets but otherwise looks like an ordinary parameter list:
func F[T any](p T) { ... }
.- These type parameters can be used by the regular parameters and in the function body.
- Types can also have a type parameter list:
type M[T any] []T
.- Each type parameter has a type constraint, just as each ordinary parameter has a type:
func F[T Constraint](p T) { ... }
.- Type constraints are interface types.
- The new predeclared name
any
is a type constraint that permits any type.- Interface types used as type constraints can embed additional elements to restrict the set of type arguments that satisfy the constraint:
- an arbitrary type
T
restricts to that type- an approximation element
~T
restricts to all types whose underlying type isT
- a union element
T1 | T2 | ...
restricts to any of the listed elements
- Generic functions may only use operations supported by all the types permitted by the constraint.
- Using a generic function or type requires passing type arguments.
- Type inference permits omitting the type arguments of a function call in common cases.
Example of a generic function
func echo[T any](v T) T {
return v
}
Note that any
is a type alias of interface{}
Example of a generic struct type
type Foo[T any] struct {
val T
}
Example of an interface constraint with type set and approximation types
type FloatingPoint interface {
~float32 | ~float64
}
Interface constraints can also be parametrized, for example a constraint that restricts any type to its pointer counterpart:
type Ptr[T any] interface {
*T
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With