Notessh2a

Generics

Generic Functions

Allow a single function to operate on multiple types while keeping compile-time type safety.

Syntax:

func functionName[T constraint](param T) returnType {
    //
}

Extra:

  • Go does not support union types directly.

    type number int | float64 // <-- syntax error

    Instead, define a type-set using an interface for use as a constraint:

    type number interface {
        int | float64
    }

    The | operator is only available in type constraints. It defines a set of allowed types for a type parameter.

    Usage:

    func add[T number](a T, b T) T {
        return a + b
    }

    The | operator can also be used inline without declaring a named constraint:

    func add[T int | float64](a T, b T) T {
        return a + b
    }
  • To include named types whose underlying type matches a constraint, use ~:

    type number interface {
        ~int | ~float64
    }

    ~int means int and any named type with underlying type int.

    type myInt int
    
    func add[T ~int | ~float64](a T, b T) T {
        return a + b
    }
    
    func main() {
    	var a myInt = 5
    	var b myInt = 10
    
    	fmt.Println(add(a, b)) // 15
    }

    Why?

    Because in Go, named types are distinct from their underlying types, even if they share the same representation.

    The ~ operator allows constraints to match based on the underlying type instead of only the exact type.

    type myInt int
    
    func main() {
    	var a myInt = 10
    	var b = 20
    
    	a = b // <-- compiler: cannot use b (variable of type int) as myInt value in assignment
    
    	a = myInt(b) // <- ok (compatible for type casting)
    
    	fmt.Println(a, b) // 20 20
    }

Examples:

  1. func main() {
        fmt.Println(add(1, 2))     // 3
        fmt.Println(add(1.5, 2.3)) // 3.8
    }
    
    func add[T int | float64](a T, b T) T {
        return a + b
    }

    Without generics:

    func main() {
        fmt.Println(addInts(1, 2))       // 3
        fmt.Println(addFloats(1.5, 2.3)) // 3.8
    }
    
    func addInts(a, b int) int {
        return a + b
    }
    
    func addFloats(a, b float64) float64 {
        return a + b
    }
  2. func main() {
        fmt.Println(swap(2.5, 5))      // 5, 2.5
        fmt.Println(swap("Hello", 99)) // 99, Hello
    }
    
    func swap[T any, U any](a T, b U) (U, T) {
        return b, a
    }
  3. type gasEngine struct {
        hp int
    }
    
    type electricEngine struct {
        kWh int
    }
    
    type car[T gasEngine | electricEngine] struct {
        model  string
        year   int
        engine T
    }
    
    func main() {
        gCar := car[gasEngine]{
            model: "Audi",
            year:  2018,
            engine: gasEngine{
                hp: 385,
            },
        }
    
        eCar := car[electricEngine]{
            model: "Tesla",
            year:  2022,
            engine: electricEngine{
                kWh: 60,
            },
        }
    
        fmt.Println(gCar, eCar) // {Audi 2018 {385}} {Tesla 2022 {60}}
    }

Generic Types

Generic types define reusable structures that works with different data types.

Syntax:

type typeName[T constraint] struct {
    fieldName T
}

Example:

  1. type List[T any] struct {
    	next *List[T]
    	val  T
    }

On this page