Notessh2a

Generics

Generic Functions

Generic functions let you use one function with different types while keeping compile-time type safety.

Syntax:

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

Extra:

  • Go does not support union types. You cannot write something like:

    type number int | float64

    Instead, you can use an interface to define a type set for use as a generic constraint:

    type number interface {
        int | float64
    }

    The | operator is only valid in type constraints. It specifies that a type parameter may be one of several types.

    then use it like:

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

    You can also use | directly without declaring a named interface:

    func add[T int | float64](a T, b T) T {
        return a + b
    }
  • If you want the constraint to also accept named types whose underlying type matches one of those types, use ~:

    type number interface {
        ~int | ~float64
    }

    Here, ~int means int and any named type whose underlying type is int, and ~float64 means float64 and any named type whose underlying type is float64.

    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, a named type is distinct from its underlying type, even if it uses the same representation.

    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 let you define a reusable type 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