Go - Generics



Generics came to Go 1.18 (March 2022) to support writing flexible, reusable code that is applicable with several types. Prior to generics, Go used interfaces or code duplication for similar use cases. It enables you to declare functions, structs, and interfaces that can work with type parameters.

Go 1.18+ release brought major features such as generics, fuzz testing, and performance enhancements, increasing flexibility and developer productivity.

Go can usually infer type parameters, so you don't have to declare them. The Generics are implemented at compile time, so they don't have runtime overhead like reflection. But overuse of generics can cause code bloat because of monomorphization (each type instantiation creates different code).

We can utilize this chapter for Data structures (such as stacks, queues, trees), Utility functions (such as Map, Filter, Reduce) and Algorithms (such as sorting, searching).

Type Parameters

Type parameters in Go allow you to define functions, structs, or interfaces that can operate on multiple types, specified using square brackets []

func Print[T any](value T) { ... }

Example

This program demonstrates the use of generics in Go by defining a generic function, 'PrintString', that prints the string representation of any type implementing the 'Stringer' interface.

package main
import "fmt"

type Stringer interface {
   String() string
}
func PrintString[T Stringer](value T) {
   fmt.Println(value.String())
}
type MyType struct {
   data string
}
func (m MyType) String() string {
   return m.data
}
func main() {
   val := MyType{data: "Hello, Generics!"}
   PrintString(val) 
}

It will generate the following output −

Hello, Generics!

Constraints

The Constraints is used to specify what types are allowed for a type parameter. Here, we use predefined constraints like any (any type) or comparable (types that support == and !=) and also we can define custom constraints using interfaces.

Example

The program demonstrates generics in Go by defining a function 'Add' that can add two numbers of any numeric type (integer or float) using type constraints.

package main
import (
   "fmt"
   "golang.org/x/exp/constraints"
)
// Add numbers of any numeric type
func Add[T constraints.Integer | constraints.Float](a, b T) T {
   return a + b
}
func main() {
   fmt.Println(Add(1, 2))
   fmt.Println(Add(1.5, 2.5))
}

We define the 'Ordered' constraint manually because the 'constraints' package is unavailable in Go versions before 1.21.

package main
import "fmt"
type Ordered interface {
    int | int8 | int16 | int32 | int64 |
        uint | uint8 | uint16 | uint32 | uint64 | uintptr |
        float32 | float64 | string
}
func Add[T Ordered](a, b T) T {
    return a + b
}
func main() {
    fmt.Println(Add(1, 2)) 
    fmt.Println(Add(1.5, 2.5)) 
}

It will generate the following output −

3
4

The first program fails because it depends on the 'constraints' package, which is not part of the standard library in Go versions before 1.21. The second program solves this by defining a custom 'Ordered' constraint, making it compatible with Go 1.18 and later.

Generic Functions

The Generic Functions are the functions that can operate on multiple types.

Example

The program demonstrates a generic function 'Print' that can print values of any type using Go's 'any' type parameter.

package main
import "fmt"

// Print any type
func Print[T any](value T) {
   fmt.Println(value)
}
func main() {
   Print(42)       
   Print("Hello")   
   Print(3.14)      
}

It will generate the following output −

42
Hello
3.14

Generic Types

The Structs, slices, or other types that can work with type parameters is called as generic types.

Example

The program defines a generic stack ('Stack[T]') that can store and retrieve elements of any type ('T'), demonstrated by pushing and popping integers and strings.

package main
import "fmt"

type Stack[T any] struct {
   items []T
}
func (s *Stack[T]) Push(item T) {
   s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() T {
   if len(s.items) == 0 {
      panic("stack is empty")
   }
   item := s.items[len(s.items)-1]
   s.items = s.items[:len(s.items)-1]
   return item
}
func main() {
   intStack := Stack[int]{}
   intStack.Push(1)
   intStack.Push(2)
   fmt.Println(intStack.Pop()) 

   stringStack := Stack[string]{}
   stringStack.Push("Go")
   stringStack.Push("Generics")
   fmt.Println(stringStack.Pop())
}

It will generate the following output −

2
Generics

You can't define generic methods, only generic functions and types. Overusing generics can make code harder to read and understand. The Go standard library has limited support for generics as of Go 1.18+.

To Learn more about this chapter, below is a list of potential additions and clarifications −

1. Type Lists in Constraints

Before Go 1.18, constraints were defined using type lists in interfaces. In Go 1.18+, the golang.org/x/exp/constraints package provides common constraints like Integer, Float, Ordered, etc.

package main
import (
   "fmt"
   "golang.org/x/exp/constraints"
)
// Max function using constraints.Ordered
func Max[T constraints.Ordered](a, b T) T {
   if a > b {
   	  return a
   }
   return b
}
func main() {
   fmt.Println("Max(3, 5):", Max(3, 5))             
   fmt.Println("Max(3.14, 2.71):", Max(3.14, 2.71))
}

It will generate the following output −

5
3.14

2. Type Sets

Constraints define a type set, which is the set of types that satisfy the constraint.

A type satisfies a constraint if it implements all the methods in the interface or belongs to the type list.

package main
import "fmt"
type Numeric interface {
	int | float64
}
func Add[T Numeric](a, b T) T {
	return a + b
}
func main() {
	fmt.Println("Add(4, 6):", Add(4, 6))           
	fmt.Println("Add(3.5, 5.5):", Add(3.5, 5.5))   
}

It will generate the following output −

Add(4, 6): 10
Add(3.5, 5.5): 9

3. Generic Methods

Go does not support generic methods directly, but you can achieve similar functionality using generic types.

package main
import "fmt"
type Box[T any] struct {
	Value T
}
func (b Box[T]) GetValue() T {
	return b.Value
}
func main() {
	box := Box[int]{Value: 42}
	fmt.Println("box.GetValue():", box.GetValue()) 
}

It will generate the following output −

box.GetValue(): 42

4. Type Inference

Go can often infer type parameters, so you don’t need to specify them. Type inference works for function calls and composite literals.

package main
import "fmt"
func Print[T any](value T) {
	fmt.Println(value)
}
func main() {
	Print(42)  
	Print("Hello")
}

It will generate the following output −

42 
Hello

5. Generic Slices and Maps

You can create generic slices and maps using type parameters.

package main
import "fmt"
func Map[T, U any](slice []T, f func(T) U) []U {
	result := make([]U, len(slice))
	for i, v := range slice {
		result[i] = f(v)
	}
	return result
}
func main() {
	nums := []int{2, 4, 6}
	doubled := Map(nums, func(n int) int {
		return n * 2
	})
	fmt.Println("doubled:", doubled) 
}

It will generate the following output −

doubled: [4 8 12]

6. Generic Channels

You can create generic channels using type parameters. Generic channels are channels in Go that use type parameters to send and receive values of a specific type.

package main
import "fmt"
func Process[T any](ch <-chan T, f func(T)) {
  for v := range ch {
  	f(v)
  }
  }

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	Process(ch, func(v int) {
		fmt.Println("Processed value:", v)
	})
}

It will generate the following output −

Processed value: 1
Processed value: 2

7. Embedding Generic Types

You can embed generic types in structs. Embedding generic types means including a generic type within another type, allowing type parameters to be inherited and reused.

package main
import "fmt"
// Generic Container struct
type Container[T any] struct {
	Value T
}
// Generic Box struct embedding Container
type Box[T any] struct {
	Container[T]
}
func main() {
	box := Box[int]{Container[int]{Value: 64}}
	fmt.Println("box.Value:", box.Value) 
}

It will generate the following output −

box.Value: 64

8. Generic Interfaces

You can define generic interfaces. It is interface that use type parameters, allowing them to work with multiple types while maintaining type safety.

package main
import "fmt"
type Stringer[T any] interface {
	String() string
}
// MyType implements Stringer
type MyType struct {
	data string
}
func (m MyType) String() string {
	return m.data
}
func PrintString[T Stringer[T]](value T) {
	fmt.Println(value.String())
}

func main() {
	val := MyType{data: "Hello, Generics!"}
	PrintString(val)
}

It will generate the following output −

Hello, Generics!

9. Error Handling

Generics can make error handling more complex, especially when working with multiple types.

Use type assertions or custom error types to handle errors in generic code.

package main
import (
   "fmt"
   "golang.org/x/exp/constraints"
)
// SafeDivide function with error handling
func SafeDivide[T constraints.Integer](a, b T) (T, error) {
   if b == 0 {
   	  return 0, fmt.Errorf("division by zero")
   }
   return a / b, nil
}
func main() {
   result, err := SafeDivide(10, 2)
   if err != nil {
   	 fmt.Println("Error:", err)
   } else {
   	 fmt.Println("SafeDivide(10, 2):", result)
   }
   
   result, err = SafeDivide(10, 0)
   if err != nil {
   	 fmt.Println("Error:", err) 
   } else {
   	fmt.Println("SafeDivide(10, 0):", result)
   }
}

It will generate the following output −

5
Error: division by zero
Advertisements