Skip to content

proposal: spec: Values assigned to multi-case branches in type switches should have generics-style  #69705

Open
@jmillikin

Description

@jmillikin

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions