Generics In Golang 1.18

Generics In Golang 1.18

When we start using Golang, The simplicity of the language and its awesome way to handle threads(goroutine), and also its speed, make us happy that our code works so fast that sometimes the result came within a nanosecond(ns). Apart from these good sides when we work on a project we sometimes copy-paste lots of code, but it's not because of a specific software engineer practicing bad code but it needs to be done because of the need. For example,

func Abs(x int64) int64 {
    if x < 0 {
        return -x
    }
    return x
}

I'm writing an absolute value printing function. The parameter I'm expecting is an int64 but If I need a similar feature for any other type like int32, int, uint, float, float32, float64, etc. In the current Go stable (<1.18) version currently, we can't do this as go didn't have any generics. A possible workaround for this feature to implement in the current stable(<1.18) release of Golang would be to use interface{}, type assertion or reflect standard package which will check the type and then decide what to do next.

import "reflect"

// interface with type assertion & reflect example
func Abs(x interface{}) interface{} {
    switch reflect.TypeOf(x).Kind() {
    case reflect.Int64:
        if x.(int64) < 0 {
            return -x.(int64)
        }
        return x.(int64)
    case reflect.Int:
        if x.(int) < 0 {
            return -x.(int)
        }
        return x.(int)
    }

    return 0
}

But this above function has some problems as it's not following the single responsibility principle from SOLID(Awesome principles to follow when developing software or any other work) principle. Another problem with using interface is that the static type checking is ignored if anyone passes multiple types in the above function.

Here comes the good news

The good news is from go 1.18 and above. Now we can define generics. It's already in the stable release of go 1.18 so anyone wish to test this feature, do so by installing the >1.18 versions. You can install go1.18 by running the below two commands then run the program by go1.18.

// command 1
go install go1.18
// command 2
go1.18 download

// running the program
go1.18 run main.go

You can also read my other blog Installing Multiple Versions of Golang using GoEnv

Generics allow our functions or data structures to take in several types that are defined in their generic form.

func Abs[T int | int32 | int64 | float32 | float64](x T) T {
    if x < 0 {
        return -x
    }
    return x
}

Some may say that this is a Syntactic Sugar of the language to give generics but the major difference between interface{} with type assertion and generics is that generics can use the static type checker to give runtime validation on the type of the parameter.

The above code may get weird as you add more and more types in it and also want to reuse the type in multiple places so we can move that to a new type like below

type Number interface {
    int | int32 | int64 | float32 | float64
}

func Abs[T Number](x T) T {
    if x < 0 {
        return -x
    }
    return x
}

Still defining all the types in that interface has lots of work but if you want to get rid of these you can use constraints

Constaints

Generics come with some constraints so we can ignore type all the types. There are a few constraints right now, maybe add more later.

  • any
  • comparable

any Constraint

any constraint, which is comparable to the empty interface{}, because it means the type in the variable could be anything.

The any constraint works great if we’re treating the value like a bucket of data, maybe we’re moving it around, but you don’t care at all about what’s in the bucket.

package main

import "fmt"

type A struct {
    Name string
}
type B struct {
    Name string
}

func Print[T any](x T) {
    fmt.Println(x)
    return
}

func main() {
    a := A{}
    a.Name = "hello"
    b := B{}
    b.Name = "world"
    Print(a)
    Print("nice")
    Print(b)
}

You can read the Generics proposal, the operations permitted for any type are as follows.

  1. Declare variables of any types
  2. Assign other values of the same type to those variables
  3. Pass those variables to functions or return them from functions
  4. Take the address of those variables
  5. Convert or assign values of those types to the type interface{}
  6. Convert a value of type T to type T (permitted but useless)
  7. Use a type assertion to convert an interface value to the type
  8. Use the type as a case in a type switch
  9. Define and use composite types that use those types, such as a slice of that type
  10. Pass the type to some predeclared functions such as new If we do need to know more about the generic types we’re working on we can constrain them using interfaces like the above.

comparable Constraint

Comparable is also a predefined containts which is allowed us use the != and == operators within your function logic

func indexOf[T comparable](s []T, x T) (int, error) {
    for i, v := range s {
        if v == x {
            return i, nil
        }
    }
    return 0, errors.New("not found")
}

func main() {
    idx, err := indexOf([]string{"pinapple", "banana", "pear"}, "banana")
    fmt.Println(idx, err) // output: 1
}

Custom Constraints

Our interface definitions, which can later be used as constraints can take their own type parameters.

type buildingUpgrader[S small, M medium] interface {
    Upgrade(S) M
}

small, medium is defined as interface.

Type lists

simply list a bunch of types to get a new interface/constraint.

// Ordered is a type constraint that matches any ordered type.
// An ordered type is one that supports the <, <=, >, and >= operators.
type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

Mixed

type ComparableStringer interface {
    comparable
    String() string
}

Self referential

Cloneable interface {
    Clone() Cloneable
}

Generic Types vs Generic Functions

So we know that we can write functions that use generic types, but what if we want to create a custom type that can contain generic types? For example, a slice of order-able objects. The new proposal makes this possible.

type comparableSlice[T comparable] []T

func allEqual[T comparable](s comparableSlice[T]) bool {
    if len(s) == 0 {
        return true
    }
    last := s[0]
    for _, cur := range s[1:] {
        if cur != last {
            return false
        }
        last = cur
    }
    return true 
}

func main() {
    fmt.Println(allEqual([]int{4,6,2}))
    // false

    fmt.Println(allEqual([]int{1,1,1}))
    // true
}

Let's implement a practical example and try to implement bubble sort using generics

import (
    "fmt"
)

type Number interface {
    int8 | int16 | int32 | int64 | float32 | float64
}

func BubbleSort[N Number](input []N) []N {
    n := len(input)
    swapped := true
    for swapped {
        swapped = false
        for idx := 0; idx < n-1; idx++ {
            if input[idx] > input[idx+1] {
                input[idx], input[idx+1] = input[idx+1], input[idx]
                swapped = true
            }
        }
    }
    return input
}

func main() {
    list := []int32{4, 3, 1, 5, 6}
    listFloat := []float32{4.3, 7.6, 2.4, 1.5}

    fmt.Println(BubbleSort(list))
    fmt.Println(BubbleSort(listFloat))
}

If you like, you can read the same article on our official blog

You can read our other official blog-posts Here

You can read my other blog-posts Here

So in conclusion we can say, Generics can give us lots of help if we can use them in development. Also, we don't need to copy/paste the same functionality again and again. Hopefully, after the >go 1.18 and above we can start using Generics. With this feature, we as Golang developers don't have to copy/paste functions for different types with the same functionality. We can reuse our code more efficiently with our loved programming language.