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.
Before 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.
What are Go generics?
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.
Why generics?
The aim of generics is to reduce boilerplate code. For example, a reverse array function doesn’t require knowing the type of element of the array, but without generics, there is no type-safe method of representing this without repetition. You instead have to implement a reverse function for each type, which will create a huge amount of code that needs to be in sync with each type implementation maintained accordingly.
Problem case: Before generics in Go
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.
A brief history of Go generics: From draft proposal to design
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:
- Functions and types can have type parameters, which are defined using optional contracts
- Contracts describe the methods required and the built-in types permitted for a type argument
- Contracts describe the methods and operations permitted for a type parameter
- Type inference will often permit omitting type arguments when calling functions with type parameters
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.
Why the draft design for Go generics changed
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.
Go generics today: Bounded type parameters
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:
- Type parameters are defined using constraints
- Interface types act as constraint for type parameters
- With interface types, we can add additional types which help in limiting the set of types that may satisfy a given constraint.
- Function and type parameters may use operators, however, this must be allowed by all types satisfying the parameter constraint.
- Type arguments can be omitted from function calls via type inference.
- Generics implementation respects the Go 1.x backwards compatibility promise
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.
Understanding generics in Go 1.18
Let’s explore some of the features of generics in Go 1.18.
Generics syntax
Go 1.18.0
introduces a new syntax for providing additional metadata about types and defining constraints on these types.
package main
import "fmt"
func main() {
fmt.Println(reverse([]int{1, 2, 3, 4, 5}))
}
// T is a type parameter that is used like normal type inside the function
// any is a constraint on type i.e T has to implement "any" interface
func reverse[T any](s []T) []T {
l := len(s)
r := make([]T, l)
for i, ele := range s {
r[l-i-1] = ele
}
return r
}

As you can see in the above image,[]
brackets are used to specify type parameters, which are a list of identifiers and a constraint interface. Here, T
is a type parameter that is used to define arguments and return the type of the function.
The parameter is also accessible inside the function. any
is an interface; T
has to implement this interface. Go 1.18 introduces any
as an alias to interface{}
.
The type parameter is like a type variable — all the operations supported by normal types are supported by type variables (for example, make
function). The variable initialized using these type parameters will support the operation of the constraint; in the above example, the constraint is any
.
type any = interface{}
The function has a return type of []T
and an input type of []T
. Here, type parameter T
is used to define more types that are used inside the function. These generic functions are instantiated by passing the type value to the type parameter.
reverseInt:= reverse[int]
(Note: When a type parameter is passed to a type, it is called “instantiated”)
Go’s compiler infers the type parameter by checking the arguments passed to functions. In our first example, it automatically infers that the type parameter is int
, and often you can skip passing it.
// without passing type
fmt.Println(reverse([]int{1, 2, 3, 4, 5}))
// passing type
fmt.Println(reverse[int]([]int{1, 2, 3, 4, 5}))
Type parameters
As you have seen in the above snippets, generics allows for reducing boilerplate code by providing a solution to represent code with actual types. Any number of type parameters can be passed to a function or struct.
Type parameters in functions
Using type parameters in functions allows programmers to write code generics over types.
The compiler will create a separate definition for each combination of types passed at instantiation or create an interface-based definition derived from usage patterns and some other conditions which are out of the scope of this article.
// Here T is type parameter, it work similiar to type
func print[T any](v T){
fmt.Println(v)
}
Type parameters in special types
Generics is very useful with special types, as it allows us to write utility functions over special types.
Slice
When creating a slice, only one type is required, so only one type parameter is necessary. The example below shows the usage for type parameter T
with a slice.
// ForEach on slice, that will execute a function on each element of slice.
func ForEach[T any](s []T, f func(ele T, i int , s []T)){
for i,ele := range s {
f(ele,i,s)
}
}
Map
The map requires two types, a key
type and a value
type. The value type doesn’t have any restraints but the key type should always satisfy the comparable
constraint.
// keys return the key of a map
// here m is generic using K and V
// V is contraint using any
// K is restrained using comparable i.e any type that supports != and == operation
func keys[K comparable, V any](m map[K]V) []K {
// creating a slice of type K with length of map
key := make([]K, len(m))
i := 0
for k, _ := range m {
key[i] = k
i++
}
return key
}
Similarly, channels are also supported by generics.
Type parameters in structs
Go allows defining structs
with a type parameter. The syntax is similar to the generic function. The type parameter is usable in the method and data members on the struct.
// T is type parameter here, with any constraint
type MyStruct[T any] struct {
inner T
}
// No new type parameter is allowed in struct methods
func (m *MyStruct[T]) Get() T {
return m.inner
}
func (m *MyStruct[T]) Set(v T) {
m.inner = v
}
Defining new type parameters is not allowed in struct methods, but type parameters defined in struct definitions are usable in methods.
Type parameters in generic types
Generic types can be nested within other types. The type parameter defined in a function or struct can be passed to any other type with type parameters.
// Generic struct with two generic types
type Enteries[K, V any] struct {
Key K
Value V
}
// since map needs comparable constraint on key of map K is constraint by comparable
// Here a nested type parameter is used
// Enteries[K,V] intialize a new type and used here as return type
// retrun type of this function is slice of Enteries with K,V type passed
func enteries[K comparable, V any](m map[K]V) []*Enteries[K, V] {
// define a slice with Enteries type passing K, V type parameters
e := make([]*Enteries[K, V], len(m))
i := 0
for k, v := range m {
// creating value using new keyword
newEntery := new(Enteries[K, V])
newEntery.Key = k
newEntery.Value = v
e[i] = newEntery
i++
}
return e
}
// here Enteries type is instantiated by providing required type that are defined in enteries function
func enteries[K comparable, V any](m map[K]V) []*Enteries[K, V]
Type constraints
Unlike generics in C++, Go generics are only allowed to perform specific operations listed in an interface, this interface is known as a constraint.
A constraint is used by the compiler to make sure that the type provided for the function supports all the operations performed by values instantiated using the type parameter.
For example, in the below snippet, any value of type parameter T
only supports the String
method — you can use len()
or any other operation over it.
// Stringer is a constraint
type Stringer interface {
String() string
}
// Here T has to implement Stringer, T can only perform operations defined by Stringer
func stringer[T Stringer](s T) string {
return s.String()
}
Predefined types in constraints
New additions to Go allow predefined types like int
and string
to implement interfaces that are used in constraints. These interfaces with predefined types can only be used as a constraint.
type Number {
int
}
In earlier versions of the Go compiler, predefined types never implemented any interface other than interface{}
, since there was no method over these types.
A constraint with a predefined type and method can’t be used, since predefined types have no methods on these defined types; it is therefore impossible to implement these constraints.
type Number {
int
Name()string // int don't have Name method
}
|
operator will allow a union of types (i.e., multiple concrete types can implement the single interface and the resulting interface allows for common operations in all union types).
type Number interface {
int | int8 | int16 | int32 | int64 | float32 | float64
}
In the above example, the Number
interface now supports all the operations which are common in provided type, like <
,>
, and +
— all the algorithmic operations are supported by the Number
interface.
// T as a type param now supports every int,float type
// To able to perform these operation the constrain should be only implementing types that support arthemtic operations
func Min[T Number](x, y T) T {
if x < y {
return x
}
return y
}
Using a union of multiple types allows the performing of common operations supported by these types and writing code that will work for all types in union.
Type approximation
Go allows creating user-defined types from predefined types like int
, string
, etc. ~
operators allow us to specify that interface also supports types with the same underlying types.
For example, if you want to add support for the type Point
with the underlining type int
to Min
function; this is possible using ~
.
// Any Type with given underlying type will be supported by this interface
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64
}
// Type with underlying int
type Point int
func Min[T Number](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
// creating Point type
x, y := Point(5), Point(2)
fmt.Println(Min(x, y))
}
All predefined types support this approximated type — the ~
operator only works with constraints.
// Union operator and type approximation both use together without interface
func Min[T ~int | ~float32 | ~float64](x, y T) T {
if x < y {
return x
}
return y
}
Constraints also support nesting; the Number
constraint can be built from the Integer
constraint and Float
constraint.
// Integer is made up of all the int types
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Float is made up of all the float type
type Float interface {
~float32 | ~float64
}
// Number is build from Integer and Float
type Number interface {
Integer | Float
}
// Using Number
func Min[T Number](x, y T) T {
if x < y {
return x
}
return y
}
constraints
package
A new package with a collection of useful constraints has been provided by the Go team — this package contains constraints for Integer
, Float
etc.
This package exports constraints for predefined types. Since new predefined types can be added to language, it is better to use constraints defined in the constraints
package. The most important one of these is the [Ordered](https://pkg.go.dev/golang.org/x/exp/constraints#Ordered)
constraint. It defines all the types that support >
,<
,==
, and !=
operators.
func min[T constraints.Ordered](x, y T) T {
if x > y {
return x
} else {
return y
}
}
Interfaces vs. generics
Generics are not a replacement for interfaces. Generics are designed to work with interfaces and make Go more type-safe, and can also be used to eliminate code repetition.
The interface represents a set of the type that implements the interface, whereas generics are a placeholder for actual types. During compilation, generic code might be turned into an interface-based implementation.
Generic syntax in Go
Functions can have optional type parameters
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.
Types can have a type parameter list
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.
Each type parameter can have an optional type constraint
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:
- Declare variables of those types
- Assign other values of the same type to those variables
- Pass those variables to functions or return them from functions
- Take the address of those variables
- Convert or assign values of those types to the type
interface{}
, and so on
In 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
}
The any
constraint
The 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.
Custom types in Go generics
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.
Examples of functions using Go generics
Let’s review some examples of how we might write a generic function in Go today.
Sorting a slice of any type
// 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 >
.
Checking if a slice contains a value
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.
Why we need Go generics
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.
When to use generics in Go
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.
When not to use generics in Go
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.
Conclusion
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.
The complete code for the article is available in this GitHub repo
Thanks for reading. If you have any questions, comments, or contributions, do not hesitate to reach out in the comment section below.