author
Kevin Kelche

Mastering Generics In Go: A Comprehensive Tutorial


Introduction

Golang generics were introduced in Go 1.18, this enables us to write generic functions and types.

NOTE: This article assumes that you have go 1.18 or later installed on your machine. If you don’t, you can download it from here.

What are Generics in Golang?

Generics are a way to write functions and types that can work with any type. In Golang generics can be defined using empty interfaces, or using type parameters and type inference.

Before the launch of go 1.18, we had to write a lot of boilerplate code to write generic functions and types. For example, let’s look at how we would write a generic function that takes a slice of any type and returns the first element of the slice.

example.go
package main

import "fmt"

func FirstInt(s []int) int {
    return s[0]
}

func FirstString(s []string) string {
    return s[0]
}

func main() {
    fmt.Println(FirstInt([]int{1, 2, 3}))
    fmt.Println(FirstString([]string{"a", "b", "c"}))
}

Copied!

In the above example, we defined two functions FirstInt and FirstString that take a slice of int and string respectively and return the first element of the slice. We then called the functions with two different types, int and string.

We could use Generic Interfaces with no constraints to achieve the same result.

example.go
package main

import "fmt"

func First[T interface{}](s []T) T {
    return s[0]
}

func main() {
    fmt.Println(First([]int{1, 2, 3}))
    fmt.Println(First([]string{"a", "b", "c"}))
}

Copied!

In the above example, we defined a generic function First that takes a slice of any type T and returns the first element of the slice. We then called the function with two different types, int and string.

Interfaces in Golang

Before we dive into generics, let’s first understand interfaces. Interfaces are a way to define a set of methods that a type must implement. For example, the io.Writer interface defines a set of methods that a type must implement to be a writer.

example.go
package main

import (
    "fmt"
    "io"
)

type Writer interface {
    Write(p []byte) (n int, err error)
}

func main() {
    var w Writer
    w = os.Stdout
    fmt.Fprintf(w, "hello, writer\n")
}

Copied!

In the above example, we defined a Writer interface that defines a Write method. We then defined a variable w of type Writer and assigned it to os.Stdout. This is possible because os.Stdout implements the Writer interface.

Generic Interfaces in Golang

Let’s now look at how we can write generic interfaces in Go. We will start by writing a generic interface that defines a Write method that takes a slice of any type and returns the number of bytes written and an error.

example.go
package main

import (
    "fmt"
    "io"
    "os"
)

type Writer[T any] interface {
    io.Writer
    Write(p []T) (n int, err error)
}

func main() {
    var w Writer[int]
    w = os.Stdout
    fmt.Fprintf(w, "hello, writer\n")
}

Copied!

Output:

output
hello, writer

Copied!

In the above example, we defined a generic interface Writer that defines a Write method that takes a slice of any type T and returns the number of bytes written and an error. We then defined a variable w of type Writer[int] and assigned it to os.Stdout. This is possible because os.Stdout implements the Writer interface.

Type inference in Golang

Let’s now look at how we can use type inference in Go. We will start by writing a generic function that takes a slice of any type and returns the first element of the slice.

example.go
package main

import "fmt"

func First[T any](s []T) T {
    return s[0]
}

func main() {
    fmt.Println(First([]int{1, 2, 3}))
    fmt.Println(First([]string{"a", "b", "c"}))
}

Copied!

Output:

output
1
a

Copied!

In this example, we defined a generic function First that takes a slice of any type T and returns the first element of the slice. We then called the function with two different types, int and string. The compiler can infer the type of the generic function from the type of arguments passed to the function.

In cases where a type has an underlying type, the compiler might not be able to infer the type of the generic function. For example, let’s look at how we would write a generic function that takes a slice of any type and returns the first element of the slice.

example.go
package main

import "fmt"

type Int int

func First[T int] (x T) {
    fmt.Println(x)
}

func main() {
    First[Int](Int(1))
}

Copied!

Output:

output
go run example.go
# constraints
./main.go:12:11: Int does not implement int (possibly missing ~ for int in constraint int)

Copied!

In this example, the generic function First has a constraint T int which means that the type of the generic function must be an int. The compiler is not able to infer the type Int from the argument passed to the function even though Int has an underlying type of int. To fix this, we can use the ~ operator to tell the compiler that Int is an int.

example.go
package main

import "fmt"

type Int int

func First[T ~int] (x T) {
    fmt.Println(x)
}

func main() {
    First[Int](Int(1))
}

Copied!

Output:

output
go run example.go
1

Copied!

Generic Functions in Golang

Let’s now look at how we can write generic functions in Go. We will start by writing a generic function that takes a slice of any type and returns the first element of the slice.

example.go
package main

import "fmt"


func First[T any](s []T) T {
    return s[0]
}

func main() {
    fmt.Println(First([]int{1, 2, 3}))
    fmt.Println(First([]string{"a", "b", "c"}))
}

Copied!

Output:

output
1
a

Copied!

In the above example, we defined a generic function First that takes a slice of any type T and returns the first element of the slice. We then called the function with two different types, int and string.

Generic Types in Golang

Generic types are types that can work with any type. Let’s look at an example of a generic type that implements the Writer interface.

example.go
package main

import (
    "fmt"
    "io"
    "os"
)

type Writer[T any] struct {
    w io.Writer
}

func (w *Writer[T]) Write(p []byte) (n int, err error) {
    return w.w.Write(p)
}

func main() {
    w := Writer[int]{os.Stdout}
    w.Write([]byte("hello"))

}

Copied!

Output:

output
hello

Copied!

In the above example, we defined a generic type Writer that implements the Writer interface. We then defined a variable w of type Writer[int] and assigned it to os.Stdout. This is possible because os.Stdout implements the Writer interface.

Generic Constraints in Golang

We can also add constraints to generic types and functions. Let’s look at an example of a generic function that takes a slice of any type that implements the io.Writer interface and returns the first element of the slice.

example1.go
package main

import (
    "io"
    "os"
)

func First[T io.Writer](s []T) T {
    return s[0]
}

func main(){
    w := First([]io.Writer{os.Stdout, os.Stderr})
    w.Write([]byte("hello"))
}

Copied!

Output:

output
hello

Copied!

example2.go
package main

import (
    "fmt"
    "io"
)

func First[T io.Writer](s []T) T {
    return s[0]
}

func main() {
    fmt.Println(First([]int{1, 2, 3}))
    fmt.Println(First([]string{"a", "b", "c"}))
}

Copied!

Output:

output
# constraints
./main.go:15:22: int does not implement io.Writer (missing Write method)
./main.go:16:22: string does not implement io.Writer (missing Write method)

Copied!

In the above two code blocks, we introduced generic constraint to the generic function First. In both examples, First takes T as a generic type that implements the io.Writer interface. In the first example, we passed a slice of io.Writer to the function and it worked as expected. In the second example, we passed a slice of int and string to the function and it failed to compile because int and string do not implement the io.Writer interface.

Generic Methods in Golang

We can also define generic methods. Let’s look at an example of a generic method that takes a slice of any type and returns the first element of the slice.

example.go
package main

import "fmt"

type Slice[T any] []T

func (s Slice[T]) First() T {
    return s[0]
}

func main() {
    fmt.Println(Slice([]int{1, 2, 3}).First())
    fmt.Println(Slice([]string{"a", "b", "c"}).First())
}

Copied!

In the above example, we defined a generic method First that takes a slice of any type T and returns the first element of the slice. We then called the method with two different types, int and string. The compiler can infer the type of the slice from the arguments passed to the method.

A method is a function with a special receiver argument. The receiver appears in its argument list between the func keyword and the method name. In the above example, the method First has a receiver of type Slice[T]. This means that the method can only be called on a variable of type Slice[T].

Generic Variadic Functions in Golang

We can also define generic variadic functions. Let’s look at an example of a generic variadic function that takes a slice of any type and returns the first element of the slice.

example.go
package main

import "fmt"

func First[T any](s ...T) T {
    return s[0]
}

func main() {
    fmt.Println(First(1, 2, 3))
    fmt.Println(First("a", "b", "c"))
}

Copied!

A variadic function is a function that takes a variable number of arguments. In Go, a variadic function is defined by adding an ellipsis (…) after the type of the last parameter. For example, the function First in the above example is a variadic function because it takes a variable number of arguments of type T.

In the example above, we defined a generic variadic function First that takes a slice of any type T and returns the first element of the slice. We then called the function with two different types, int and string.

Generic Variadic Methods in Golang

We can also define generic variadic methods. Let’s look at an example of a generic variadic method that takes a slice of any type and returns the first element of the slice.

example.go
package main

import "fmt"

type Slice[T any] []T

func (s Slice[T]) First() T {
    return s[0]
}

func main() {
    fmt.Println((Slice[int]{1, 2, 3}.First()))
    fmt.Println((Slice[string]{"a", "b", "c"}.First()))
}

Copied!

In the above example, we defined a generic variadic method First that takes a slice of any type T and returns the first element of the slice. We then called the method with two different types, int and string. The compiler can infer the type of the slice from the arguments passed to the method.

Generic Variadic Types in Golang

We can also define generic variadic types. Let’s look at an example of a generic variadic type that implements the Writer interface.

example.go
package main

import (
    "fmt"
    "io"
    "os"
)

type Writer[T any] struct {
    w io.Writer
}

func (w *Writer[T]) Write(p []byte) (n int, err error) {
    return w.w.Write(p)
}

func main() {
    w := Writer[int]{w: os.Stdout}
    w.Write([]byte("Hello, world!\n"))
}

Copied!

In the above example, we defined a generic variadic type Writer that implements the Writer interface. We then defined a variable w of type Writer[int] and assigned it to os.Stdout. This is possible because os.Stdout implements the Writer interface. We then called the Write method on the variable w and passed a slice of bytes to it.

Generic Variadic Constraints in Golang

We can also add constraints to generic variadic types and functions. Let’s look at an example of a generic variadic function that takes a slice of any type that implements the io.Writer interface and returns the first element of the slice.

example.go
package main

import (
    "fmt"
    "io"
    "os"
)

func First[T io.Writer](s ...T) T {
    return s[0]
}

func main() {
    // assign the first writer to w
    w := First(os.Stdout, os.Stderr, )
    // print to w
    fmt.Fprintln(w, "Hello, world!")
}

Copied!

Here function First takes a slice of any type T that implements the io.Writer interface and returns the first element of the slice. We then called the function with two different types, os.Stdout and os.Stderr. It worked as expected because both os.Stdout and os.Stderr implement the io.Writer interface.

Generic Type Sets in Golang

Now that we have looked at how generics allow us to define both with constraints and without constraints, we realize that these are two complete extremes. You may ask yourself, what if I want to define a generic type that can only be used with a subset of types? For example, what if I want to define a generic type that can only be used with types that implement the int and string interfaces? This is where type sets come in.

Type sets are a set of types that can be used in a generic type, function, method, or variadic function or method. Let’s look at an example of a generic type that can only be used with types that implement the int and string interfaces.

example.go
package main

import "fmt"

type FirstTypes interface{
    ~int | ~string
}

type First[T FirstTypes] struct {
    v T
}

func (f First[T]) Value() T {
    return f.v
}

func main() {
    fmt.Println(First[int]{v: 1}.Value())
    fmt.Println(First[string]{v: "a"}.Value())
}

Copied!

Output:

output
1
a

Copied!

If we introduced a foreign type to the function implementation, the compiler would throw an error.

output
# constraints
./main.go:21:20: bool does not implement FirstTypes (bool missing in ~int | ~string)

Copied!

Conclusion

Golang is a statically typed language. This means that the type of a variable is known at compile time. In this article, we looked at how to define generic types, functions, methods, and variadic functions and methods in Golang. We also looked at how to add constraints to generic types, functions, methods, and variadic functions and methods.

Subscribe to my newsletter

Get the latest posts delivered right to your inbox.