Description
Go Programming Experience
Experienced
Other Languages Experience
C, C++, Python, Rust, Haskell
Related Idea
- Has this idea, or one like it, been proposed before?
- Does this affect error handling?
- Is this about generics?
- Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit
Has this idea, or one like it, been proposed before?
#65031 seems similar, although the details differ.
#57644 is possibly related, but I can't tell from its description what effect (if any) it would have on type switches.
Does this affect error handling?
No
Is this about generics?
Yes -- this proposal is to make multi-case type switches match the semantics of generic functions with a set of permitted types.
Proposal
In Go 1.23, a value assigned as the match result in a multi-case type switch retains the original type being inspected. This prevents it from being passed to a function with a type parameter constraint that's only satisfied by the narrowed type.
func fmtUint[T uint8 | uint16 | uint32 | uint64](value T) string {
return strconv.FormatUint(uint64(value), 10)
}
// error: any does not satisfy uint8 | uint16 | uint32 | uint64
func fmtValue(value any) string {
switch v := value.(type) {
case uint8, uint16, uint32, uint64:
// `v` is known to be of the above listed concrete types,
// but its type remains `interface{}`, so it can't be used as `T`.
return fmtUint(v)
}
return fmt.Sprintf("%v", value)
}
// semantically equivalent, and valid in Go 1.23, but overly verbose
func fmtValue(value any) string {
switch v := value.(type) {
case uint8:
return fmtUint(v)
case uint16:
return fmtUint(v)
case uint32:
return fmtUint(v)
case uint64:
return fmtUint(v)
}
return fmt.Sprintf("%v", value)
}
I propose to change the semantics of multi-case type switches so that the type of the matched variable becomes the intersection of the matched types, plus the original type:
interface {
// To preserve existing behavior, `v` can be treated as its original type.
any
// `v` can also satisfy type parameter conditions that permit all of the types
// in its case match.
( uint8 | uint16 | uint32 | uint64 )
}
The matched case must have types that are a subset of the generic type:
// OK: `v` might be a `uint8` or `uint16`, but both of those are acceptable to `fmtUint`
func fmtValue(value any) string {
switch v := value.(type) {
case uint8, uint16:
return fmtUint(v)
}
return fmt.Sprintf("%v", value)
}
// Error: `v` might be `uintptr`, so the set of possible types is a superset of those
// accepted by`fmtUint`.
func fmtValue(value any) string {
switch v := value.(type) {
case uint8, uint16, uint32, uint64, uintptr:
return fmtUint(v)
}
return fmt.Sprintf("%v", value)
}
The type of v
should also work with the semantics of a generic type parameter for regular code within the case, for example performing conversions that are valid for all matchable types:
// OK: all of the matched types can be cast to uint64.
func fmtValueHex(value any) string {
switch v := value.(type) {
case uint8, uint16, uint32, uint64:
return strconv.FormatUint(uint64(v), 16)
}
return fmt.Sprintf("%v", value)
}
Language Spec Changes
No response
Informal Change
No response
Is this change backward compatible?
I think so? Given existing and proposed behavior, I believe that any existing code would continue to compile and run without changes.
Orthogonality: How does this change interact or overlap with existing features?
No response
Would this change make Go easier or harder to learn, and why?
No response
Cost Description
No response
Changes to Go ToolChain
No response
Performance Costs
No response
Prototype
No response