DEV Community 👩‍💻👨‍💻

nadirbasalamah
nadirbasalamah

Posted on

Generics in Go Tutorial

Introduction

In the Go programming language with the version of 1.18 or later there is a new feature called Generics. Generics are a feature that has been featured in many programming languages like Java. Generics are a feature to define a type or data type as a parameter in a function or a struct.

Using Generics in Function

The generics mechanism can be applied in a function. To use a generics feature, the any keyword can be used to define all data types that can be used as a parameter in a function or a struct. The other keyword that can be used is comparable which means the data type that implements comparable like int and float64.

This is an example of generics usage in a function. Before the generics are used, two functions are created called compareInt and compareFloat to compare two numbers for different data types including int and float64.

// compare two numbers for int data type
func compareInt(x, y int) bool {
    return x == y
}

// compare two numbers for float64 data type
func compareFloat(x, y float64) bool {
    return x == y
}

Enter fullscreen mode Exit fullscreen mode

This is the example of generics usage in function to compare two numbers.

func compare[T comparable](x, y T) bool {
    return x == y
}

Enter fullscreen mode Exit fullscreen mode

Based on the code above, the generics are implemented in [T comparable] syntax. The T is a variable name for the generic parameter. The variable name for the generic parameter can contain many characters like numType but the parameter name in general only contains a single character. The comparable syntax means a function can be used by a data type that implements comparable like int and float64.

This is an example of a function call that implements the generics.

package main

import "fmt"

func main() {
    fmt.Println("compare result for int: ", compare(2, 3))
    fmt.Println("compare result for float64: ", compare(2.9, 2.9))
}

Enter fullscreen mode Exit fullscreen mode

Output.

compare result for int:  false
compare result for float64:  true
Enter fullscreen mode Exit fullscreen mode

The generics can be used to define the required data types for the function with the | operator. This is the example of generics usage in the average() function.

// calculate the average
// T is an int or float64 data type
func average[T int | float64](nums []T) T {
    var sum T = 0

    for _, num := range nums {
        sum += num
    }

    var result T = sum / T(len(nums))

    return result
}

Enter fullscreen mode Exit fullscreen mode

Based on the code above, the usage of the generic can be seen in [T int | float64] syntax which means the function only can be used by the int or float64 data type.

This is the example of the average() function call.

package main

import "fmt"

func main() {
    var intData []int = []int{1, 2, 3, 4}
    var floatData []float64 = []float64{2.5, 3.4, 6.7, 2.9}

    fmt.Println("average result for int data type: ", average(intData))
    fmt.Println("average result for float64 data type:", average(floatData))
}

Enter fullscreen mode Exit fullscreen mode

Output.

average result for int data type:  2
average result for float64 data type: 3.8750000000000004
Enter fullscreen mode Exit fullscreen mode

Using Generics in Struct

The generics can be applied in a struct. The generics usage is similar in a function by using certain keywords like any, and comparable and defining the required data types with the | operator.

This is the example of a struct without generics implementation.

type IntBox struct {
    Content int
}

type StringBox struct {
    Content string
}

Enter fullscreen mode Exit fullscreen mode

This is the example of generics usage in a struct called Box.

type Box[T any] struct {
    Content T
}

Enter fullscreen mode Exit fullscreen mode

Based on the code above, the usage of the generic can be seen in [T any] syntax which means the struct can be used with any data type.

This is an example of struct usage that implements generics.

package main

import "fmt"

type Box[T any] struct {
    Content T
}

func main() {
    var numberBox Box[int] = Box[int]{
        Content: 200,
    }

    var stringBox Box[string] = Box[string]{
        Content: "my content",
    }

    fmt.Println(numberBox.Content)
    fmt.Println(stringBox.Content)
}

Enter fullscreen mode Exit fullscreen mode

Output.

200
my content
Enter fullscreen mode Exit fullscreen mode

This is another example of generics usage in a struct with two generic parameters.

type Pair[K, V any] struct {
    Key   K
    Value V
}

Enter fullscreen mode Exit fullscreen mode

The number of the generic parameter is not restricted. In general, the number of the generic parameter is one or two parameters.

This is an example of struct usage that used two generic parameters.

package main

import "fmt"

type Pair[K, V any] struct {
    Key   K
    Value V
}

func main() {
    var pairOne Pair[string, int] = Pair[string, int]{
        Key:   "score",
        Value: 2000,
    }

    var pairTwo Pair[int, string] = Pair[int, string]{
        Key:   10,
        Value: "a value",
    }

    fmt.Println(pairOne.Key, pairOne.Value)
    fmt.Println(pairTwo.Key, pairTwo.Value)
}

Enter fullscreen mode Exit fullscreen mode

Output

score 2000
10 a value
Enter fullscreen mode Exit fullscreen mode

Using Generics in Interface

The generics can be used with the interface. This is an example of generics usage in an interface.

// Number is an int or float64 data type
type Number interface {
    int | float64
}

Enter fullscreen mode Exit fullscreen mode

Based on the code above, the interface called Number indicates a certain data type that has an int or float64 data type.

The interface that uses the generics can be applied in a function or a struct. This is an example of generic interface usage in a function.

// calculate average
// using Number interface as a type
func average[T Number](nums []T) T {
    var sum T = 0

    for _, num := range nums {
        sum += num
    }

    var result T = sum / T(len(nums))

    return result
}

Enter fullscreen mode Exit fullscreen mode

Based on the code above, the Number interface is used inside the average() function which means the average function can be called or used by a data type that implements the Number interface including int or float64 data type. With the interface, the code is more readable and well structured.

Notes

  • The generics can be used to reduce code duplication if the code has a similar logic structure. For example, a single generic function to calculate an average can be created instead of creating many average functions for many data types.
// without generics, each average function is created for each data type
func averageInt(nums []int) int {
    var sum int = 0

    for _, num := range nums {
        sum += num
    }

    var result int = sum / len(nums)

    return result
}

func averageFloat(nums []float64) float64 {
    var sum float64 = 0

    for _, num := range nums {
        sum += num
    }

    var result float64 = sum / float64(len(nums))

    return result
}

Enter fullscreen mode Exit fullscreen mode
// with generics, the average function can be used for int and float64 data type
func average[T int | float64](nums []T) T {
    var sum T = 0

    for _, num := range nums {
        sum += num
    }

    var result T = sum / T(len(nums))

    return result
}

Enter fullscreen mode Exit fullscreen mode

Sources

I hope this article is helpful to learn generics in Go. If you have any thoughts, you can write it in the discussion section below.

Top comments (4)

Collapse
jhelberg profile image
Joost Helberg

Nice article, learned some new stuff.

Collapse
nadirbasalamah profile image
nadirbasalamah Author

Thank you

Collapse
folafunmidb profile image
Folafunmi

Nice article.

Collapse
nadirbasalamah profile image
nadirbasalamah Author

Thank you

🌚 Browsing with dark mode makes you a better developer by a factor of exactly 40.

It's a scientific fact.