What's the purpose of a blank identifier in a type parameter?

Issue

What’s the difference between these two functions?

func f[_ string, p string](s ...p) {
    fmt.Println(s)
}
func f[p string](s ...p) {
    fmt.Println(s)
}

Why even put a blank identifier in a type parameter in the first place?

Solution

Using underscore _ in place of a type parameter name simply signals that the type parameter is not used within the function scope.

In your example code, it doesn’t really make a difference; both versions of f can be called as f("blah") without supplying explicit type parameters. But this is possible only because the constraint string restricts to an exact type. It can only ever be string, so type inference still works.

If you change it to an approximate type instead it has to be instantiated explicitly:

// can't infer first type param from anywhere
func f[_ ~string, P ~string](s ...P) {
    fmt.Println(s)
}

func main() {
    // must supply first type param, may omit second one
    f[string]("blah")
}

In real world scenarios where constraints are not exact types, the underscore would likely force callers to specify the type parameter: this effectively would break backward compatibility, because client code must indeed be updated to work.

Just to be clear, this applies to type parameters that are independent of each other. If the underscored type parameter can be inferred from another one, it can still compile without explicit instantiation:

// called as foo("blah")
func foo[T ~string, P *T](t T) {
    var p P = &t
    fmt.Println(t, p)
}

// still called as foo("blah")
func foo[T ~string, _ *T](t T) {
    fmt.Println(t)
}

However it’s worth noting that if a function has two independent type parameters that are inferrable from arguments:

func f[T any, U any](t T, u U) {
    fmt.Println(t, u)
}

…making U not inferrable implies also removing the u U argument too, so you already have a backward-incompatible change.

// one less argument, requires major version upgrade anyway
func foo[T any, _ any](t T) {
    fmt.Println(t, u)
}

With methods instead, it’s a different story. Since methods can’t specify type parameters that weren’t declared on the receiver type, using underscore is a much more frequent occurrence. It may just happen that some methods do not need all type parameters. Then the underscore aptly reflects that:

type Foo[T,U any] struct {
    ID  T
    Val U
}

// we do not need to reference U here
func (f *Foo[T,_]) SetID(t T) {
    f.ID = t
}

// we do not need to reference T here
func (f *Foo[_,U]) SetVal(v U) {
    f.Val = v
}

Answered By – blackgreen

Answer Checked By – Senaida (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.