Editor’s note: This post was updated on 10 June 2022 to include information about generics in the Go v1.18 release.
Generic programming is also known as parametric polymorphism in other programming languages. It is a way of reducing function duplication by writing “common functions” with support for multiple type parameters/arguments.
In this case, the type parameters can be factored out or, better still, isolated, and the functions can still run irrespective of the type arguments we pass.
As a feature of the Go programming language, generics were not included in the initial release version (Go1.x), nor in the design of the language.
However, after much debate via draft proposals and design documents among the Go community — and hard work from the core team and contributors — support for generics has now been added to the language core in the Go v1.18 release earlier this year.
With that in mind, in this post, we are going to cover:
any
constraintBefore we begin, readers must understand the basics of the Go programming language. Specifically, readers should have an understanding of the Go type system, including type assertions and type inference, as well as interfaces and their usage in Go programs.
Every statically typed language has generics in one form or another. Generics offer us a new way to express type constraints in Go code. The overall purpose of generics in Go is to avoid boilerplate code or duplication of logic.
The Go authors had to weigh the pros and cons of generics in a programming language like Go, which was designed for networked system software. As a result, they initially opted out of features like concurrency, scalable builds, and so on.
The goal of adding generics to Go is to be able to write libraries or functions that would work or operate on arbitrary types or values. Therefore, the language should be able to record constraints on type parameters explicitly.
Another frequent request for generics in Go is the ability to write compile-time type-safe containers.
In summary, the aim is to support writing Go libraries that abstract away needless type detail by allowing parametric polymorphism with type parameters.
One of Go’s key distinguishing features is its approach to interfaces, which are also targeted at code reuse. Specifically, interfaces make it possible to write abstract implementations of algorithms.
Go already supported a form of generic programming via the use of empty interface types. For example, we can write a single function that works for different slice types by using an empty interface type with type assertions and type switches.
The empty interface type lets us capture different types via type switches and type assertions. We can write functions that use those interface types, and those functions will work for any type we pass as arguments.
But this method does not support code reuse. With interfaces, we have to write the switch cases for every type we want to support. We have to then make use of type assertions and case statements where we check based on the type we want to support.
For example, to reverse a slice of any element type, we can pass an empty interface as a parameter, and making use of type assertions, we can get the type we pass as arguments when we call the function.
Let us look at a quick example of a non-generic function below:
func Sum(args …int) int { var sum int for i := 0; i < len(args); i++ { sum += args[i] } return sum }
What happens if we intend to implement this same feature for an int32 or an int64 data type? See below:
func SumInt64(args ...int64) int64 { var sum int64 for i := 0; i < len(args); i++ { sum += args[i] } return sum }
We would need to implement separate functions to handle each data type. With the release of generics, this is no longer needed.
According to an official Go blog post from 2019, generics support has always been one of the top problems to fix in the language.
The initial design draft for Go generics, which was refined and updated several times over the years, introduced contracts
. The aim of contracts
was to validate a set of type arguments to a function, as well as what methods can be called on those types.
The draft design introduced the idea of a named contract, where a contract is like a function body illustrating the operations that type must support.
In summary, the goal was for the implementation to be able to constrain the possible types that can be used. An example from the draft shows how to declare that values of type T
must be comparable:
contract Equal(t T) { t == t }
Then, to require a contract, we give its name after the list of type parameters:
type Set(type T Equal) []T
Finally, to use the defined contract
in a function signature:
// Find returns the index of x in the set s, // or -1 if x is not contained in s. func (s Set(T)) Find(x T) int { for i, v := range s { if v == x { return i } } return -1 }
In the function definition above, we can see that we are making use of the defined contract, which specifies that the types must be comparable.
The summary of the contract design draft outlines that:
You can read more about the earlier design draft in an article by Russ Cox, wherein he discusses and experiments with the idea of contracts extensively.
The problem with contracts was that the design seemed to introduce more complexities to the language.
According to a draft design from 2019, since type lists appeared only in contracts rather than on interface types, many people had a hard time understanding this difference. The Go team therefore dropped the idea of contracts entirely.
Instead, the team simplified their approach to use only interface types because it also turned out that contracts could be represented as a set of interfaces.
The type parameters proposal for adding generic programming to Go emphasizes optional type parameters or parameterized types. Here is an outline of this proposed and accepted design:
Before the syntax for generics in Go was added, every function we wrote in Go had to apply only to a particular type.
For example, it was not possible to write a simple copy
function that can operate on any container type, e.g., map
. We had to write separate functions, with almost the same signature except for the types, for the different target types.
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. Type parameters are bounded by explicitly defined structural constraints.
Let’s explore some of the features of generics in Go 1.18.
Let’s start with the keyword type
— i.e., func Add\[T any\](a, b T) T
. An example below shows the kind of function that is permitted in generic programming:
func Sum[T int](args ...T) T { var sum T for i := 0; i < len(args); i++ { sum += args[i] } return sum }
As we can see from the generic function above, [T int]
represents the basic syntax for writing generic code in Go. T
is the generic type and int
is the constraint to that type. The constraint ensures type safety is guaranteed by specifying allowed type parameters.
The challenge here was about how the type parameter should be declared, since every identifier has to be declared somehow. The resolution was that type parameters were similar to ordinary non-type function parameters and, therefore, should be listed along with other parameters.
These type parameters (distinguishable from non-type parameters) can be used by the regular parameters and in the function body.
Within the function Print
, the identifier T
is a type parameter: a type that is currently unknown but that will be known when the function is called. This parameter list appears before the regular parameters.
func Print(T int | int64)(args ...T) T{ // same as above }
Since Print
has a type parameter, any call of Print
must provide a type argument.
func main() { fmt.Println(Sum([]int{1, 2, 3}...)) fmt.Println(Sum([]int64{1, 2, 3}...)) }
Type arguments are passed as a separate list of arguments.
If a generic function does not specify a constraint for a type parameter, as is the case for the Print
method above, then any type argument is permitted for that parameter.
The only operations that generic functions can use with values of that type parameter are those operations that are permitted for values of any type. The operations permitted for any type are:
interface{}
, and so onIn defining constraints, Go already has a construct that comes pretty close to what is needed for a constraint: an interface type. In this new design, constraints are equivalent to interface types.
Constraints are an interface type that function parameters have to fulfill. In a case where we have too many constraints, we can use the any
keyword or abstract our type constraint as an interface type.
Additionally, writing a generic function is like using values of the interface type: the generic code can only use the operations permitted by the constraint (or operations that are permitted for any type).
Here’s another example of a constraint below:
type Sumable interface { int | int64 | uint32 }
Although generic functions are not required to use constraints, they can be listed in the type parameter list as a metatype.
func Sum\[T Sumable\](args ...T) T { var sum T for i := 0; i < len(args); i++ { sum += args[i] } return sum }
any
constraintThe any
constraint is equivalent to an empty interface type interface{}. The problems with this is that an empty interface{} tells us nothing about the data and it is less safer, and requires too many error handling. See example below without type assertions:
func Sum\[T any\](args ...T) T { var sum T for i := 0; i < len(args); i++ { sum += args[i] } return sum }
Note: The above program will compile with an error because the compiler does not know anything about T
or if it supports the addition operator.
Go generics includes support for custom type parameters:
type CustomIntType int func main() { fmt.Println(Sum([]CustomIntType{1, 2, 3}...)) }
Note that each type parameter may have its own constraint. Type parameters that do not have a constraint must have the constraint of an empty interface{}
type.
Additionally, a single constraint can be used for multiple type parameters. However, the constraint applies to each of the type parameters separately.
Let’s review some examples of how we might write a generic function in Go today.
// import the constraint package func sortSlice\[T constraints.Ordered\](s []T) { sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) } stringSlice := []string{"o", "a", "b"} sortSlice(stringSlice) fmt.Println(stringSlice) //[a b o] intSlice := []int{0, 3, 2, 1, 6} sortSlice(intSlice) fmt.Println(intSlice) // [0 1 2 3 6]
The Ordered
constraint permits any ordered type; that is, any type that supports the operators less than <
, less than or equal to <=
, greater than or equal to >=
, or greater than >
.
As we can see in the example below, calling a generic function looks just like calling any other function:
func contains\[T comparable\](elems []T, v T) bool { for _, s := range elems { if v == s { return true } } return false } func main() { fmt.Println(contains([]string{“e”,”f”, “g”}, “f”)) // true fmt.Println(contains([]int{5, 6, 7}, 8)) // false }
In a dynamically typed language like Python or JavaScript, we can simply write the function without bothering to specify the element type. This doesn’t work in Go since it is statically typed and requires that we write down the exact type of the slice and of the slice elements.
Finally, generics make it easier to write functions once to use everywhere. Functions that work on specific data types, like copy
or string
data types, can be made generic and used on map
types as well. They can even be extended to user defined data types.
Generic programming enables the representation of algorithms and data structures in a generic form, with concrete elements of the code (such as types) factored out.
Generics in Go encompass the above definition and also entail a programming style where types are abstracted from function definitions and data structures. You may know we could also do this with interfaces
and type assertions
in Go, but we would have to write the same methods for all the types.
The idea here is to be able to factor out the element type. This will allow us write the function once, write the tests once and bundle them as a Go package. Then, we can reuse an efficient, fully debugged implementation that works for any value type whenever we want.
You should use Go generics if you are struggling to implement a solution that requires a singularity of behavior for different data types. This means that you want to define a set of behaviors or methods for all the different data types you want to store or pass around in a polymorphic way.
Go generics are also useful if you are reaching for an empty interface type when you do not intend to do some data validation or use any Marshal or Unmarshal operations.
Finally, use Go generics when writing functions that operate on container types like maps, slices, channels, or any other general-purpose data structure.
You should not use Go generics when calling a method on a type argument. Instead, use an interface type; e.g., replacing the io.Reader
and io.Writer
interface types
Also, method sets in Go have different implementations, meaning generics would be the wrong choice in this case.
Finally, don’t use Go generics when performing different operations for each type, even within method sets. In this case, use Go’s reflection API instead, as found in the JSON encoding and decoding package (decode.go
) file.
Generic data structure and algorithms provide many advantages, including flexibility and code reusability via utility functions or writing algorithms that can then be applied on different type arguments.
Generics make Go safer, more efficient to use, and more powerful. This will allow us to implement many problems as functions that would apply to different types and use previously debugged, optimized, and efficient packages or libraries, thus allowing for greater scalability and easier handling of a growing codebase.
Thanks for reading. If you have any questions, comments, or contributions, do not hesitate to reach out in the comment section below or, alternatively, you can message me on Twitter.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
2 Replies to "Go generics: Past designs and present release features"
Just a few small things I found misleading. First you say that contracts have been dropped. They have not been dropped but replaced with constraints – different syntax, different name but the same purpose. You also say that multiple type parameters are now allowed but multiple type parameters have been a part of all the draft designs.
generics in go would be pretty pog ngl