When specifying the type parameters for a generic structure in Go, you can use the notation ~T to mean "any type whose underlying structure is equivalent to T". For example, given:
func F[X ~string](x X) {}the type parameter X can be instantiated with any type whose underlying representation is string. That includes string itself, of course, along with types like type Foo string which are based on it.
When the type constraint is based on a concrete type, as in this example, the value of the ~ notation is clear. However, you will also see examples like this, drawn from the slices package:
func Clip[S ~[]E, E any](s S) SIn plain language, this allows S to be any type whose underlying structure is a slice of E. A natural question that sometimes comes up in discussion is why this could not be more simply written as:
func ClipE[E any](es []E) []EAnd indeed ClipE will accept any argument that could have been passed to the original Clip:
ClipE([]int{1, 2, 3})
type Q struct{ X bool }
ClipE([]Q{{true}, {true}, {false}})
type QS []Q
ClipE(QS{{false}, {false}, {true}})The important difference between the two is not in their arguments, but in their results. By declaring a type constraint S ~[]E, the Clip function ensures that its return value has the same type as its argument.
By contrast, regardless what its argument type is, the result of ClipE has type []E (for some E).
To see the difference, consider this example:
func Clip[S ~[]E, E any](s S) S { return s }
func ClipE[E any](es []E) []E { return es }
type QS []int
es := QS{1, 2, 3}
fmt.Printf("es=%T, Clip(es)=%T\n", es, Clip(es))
fmt.Printf("es=%T, ClipE(es)=%T\n", es, ClipE(es))
// Output:
// es=main.QS, Clip(es)=main.QS
// es=main.QS, ClipE(es)=[]intHere we see that Clip correctly propagates the argument type (main.QS) to the return value, while ClipE discards the named type and returns a plain []int.
So, in summary, underlying type constraints can be useful whenever you need to declare a function that needs to preserve the actual (not just underlying) type of its arguments in its results.
Although the example above uses a function, this applies to methods also, e.g.,
type T[X any, Y ~[]X] struct {
Thing Y
}
func (t *T[X, Y]) Foo(p Y) Y {
old := t.Thing
t.Thing = p
return old
}