Instantiations
A generic function or type is instantiated by substituting type arguments for the type parameters. Instantiation proceeds in two steps:
- Each type argument is substituted for its corresponding type parameter in the generic declaration. This substitution happens across the entire function or type declaration, including the type parameter list itself and any types in that list. After substitution, each type argument must satisfy the constraint (instantiated, if necessary) of the corresponding type parameter. Otherwise instantiation fails.
- Instantiating a type results in a new non-generic named type; instantiating a function produces a new non-generic function.
type parameter list type arguments after substitution [P any] int int satisfies any [S ~[]E, E any] []int, int []int satisfies ~[]int, int satisfies any [P io.Writer] string illegal: string doesn't satisfy io.Writer [P comparable] any any satisfies (but does not implement) comparable
Earlier this week, I took advantage of these rules to add some compile-time safety to what would otherwise be a runtime panic. The code I was working on uses the Merge function from the dario.cat/mergo library. Here’s what the function does (with a key sentence emphasized):
Merge will fill any empty for value type attributes on the dst struct using corresponding src attributes if they themselves are not empty. dst and src must be valid same-type structs and dst must be a pointer to struct. It won’t merge unexported (private) fields and will do recursively any exported field.
This library was written long before generics existed, so I added a small wrapper function:
func Merge[S any](dst *S, src S) error {
return errors.WithStack(mergo.Merge(dst, src))
}
Why? What does this seemingly unused S
type parameter provide?
Well, I had just seen my program crash after an intended refactoring. What had been a valid call: Merge(&dst, src)
, had stopped working when I made a change to the type of src
. But my compiler (and thus IDE) didn’t know about it.
So I added the wrapper with a type parameter to effectively provide some compile-time safety, which would cause the instantiation to fail according to the above rules if I made the same mistake again. Now the function signature enforces two key parts of the emphasized sentence above: “dst and src must be valid same-type” and “dst must be a pointer”. Notably it does not enforce that dst and src are structs. There’s no way to express that rule using Go generics. But even without that, this new compile-time check easily pointed out where I had failed to update the Merge
call after changing src
’s type in a few more functions during my refactoring.
Quotes from The Go Programming Language Specification Version of August 2, 2023