Go Generics – Unions

If you come across this Q&A because of its generic title (pun not intended), here’s a quick primer about unions:

  1. Can be used to specify the type set of an interface constraint. A generic type parameter T will be restricted to the types in the union
  2. Can be used only in interface constraints. And if an interface contains a union (with one or more terms) then it is an interface constraint.
  3. Can include approximate elements with ~

For example:

type intOrString interface {
    int | string
}

func Foo[T intOrString](x T) {
    // x can be int or string
}

Now onto the OP’s question, with some more details:

You can’t use an interface constraint as a type

By including a type set, intOrString becomes an interface constraint, and using it as a type is explicitly not supported. Permitting constraints as ordinary interface types:

This is a feature we are not suggesting now, but could consider for later versions of the language.

So the first thing to do is to use intOrString as an actual constraint, hence use it in a type parameter list. Below I replace comparable with intOrString:

type testDifferenceInput[T intOrString] [][]T
type testDifferenceOutput[T intOrString] []T
type testDifference[T intOrString] struct {
    input testDifferenceInput[T]
    output testDifferenceOutput[T]
}

This also means you can’t use the constraint to instantiate a concrete type as your test slice:

// bad: using intOrString to instantiate a parametrized type
[]testDifference[intOrString]

A generic container can’t hold items of different types

The second problem you have is that the test slice contains two structs of unrelated types. One is testDifference[int] and one is testDifference[string]. Even though the type testDifference itself is parametrized with a union constraint, its concrete instantiations are not the same type. See also this for further details.

If you need a slice holding different types, your only option is []interface{} (or []any) …or, you just separate the slices:

ttInts := []testDifference[int]{ testDifference[int]{...}, /* etc. */ }
ttStrs := []testDifference[string]{ testDifference[string]{...}, /* etc. */ }

Allowed operations on union constraints

Only the operations supported by all types in the type set. Operations based on type sets:

The rule is that a generic function may use a value whose type is a type parameter in any way that is permitted by every member of the type set of the parameter‘s constraint.

In case of a constraint like int | string, what are the operations permitted on either int or string? In short:

  • var declaration (var foo T)
  • conversions and assertions T(x) and x.(T), when appropriate
  • comparison (==, !=)
  • ordering (<, <=, > and >=)
  • the + operator

So you can have an intOrString constraint, but the functions that make use of it, including your func Difference, are limited to those operations. For example:

type intOrString interface {
    int | string
}

func beforeIntOrString[T intOrString](a, b T) bool {
    return a < b
}

func sumIntOrString[T intOrString](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(beforeIntOrString("foo", "bar")) // false
    fmt.Println(beforeIntOrString(4, 5)) // true

    fmt.Println(sumIntOrString("foo", "bar")) // foobar
    fmt.Println(sumIntOrString(10, 5)) // 15
}

Leave a Comment