Description
Go Programming Experience
Experienced
Other Languages Experience
Go, Rust, TS/JS, C/C++, Python
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?
No.
Does this affect error handling?
No.
Is this about generics?
No.
Proposal
// typ is of type types.Type
switch newTyp := typ.(type) {
case *types.Named, *types.Interface:
}
Currently newTyp in the case statement has the types.Type
type:
type Type interface {
Underlying() Type
String() string
}
Proposed change:
Instead it should have the type of an implicitly generated interface, which consists of the intersection of all methods both types have in common.
In current go this would be:
switch newTyp := typ.(type) {
case *types.Named, *types.Interface:
{
newTyp := newTyp.(interface {
Method(i int) *types.Func
NumMethods() int
Underlying() types.Type
String() string
})
}
}
I know that this helps people who analyze go code, because it allows for more concise type switches, which are very common when analyzing code. There are probably other use cases, like parsers, anytime when there is a type switch to narrow down the type and then calling a method on that type.
It would also be nice if this works for the intersection of struct fields, but I think additional work on generics is required first, in order to have a similarly low cost "syntactic sugar" implementation.
Language Spec Changes
This new behavior would need to be documented under https://go.dev/ref/spec#Type_switches
Informal Change
In case statements with multiple types, the switched type is of an interface which contains the intersection of both types methods.
Is this change backward compatible?
Strictly speaking no, because the output of reflect.TypeOf
may change, however I don't think this will break programs. reflect.TypeOf
could pretend that the implicitly generated interface doesn't exist and tell the caller that typ
is of type types.Type
, then it would be backward compatible.
Before the change (this is real code from here):
switch typ := typ.(type) {
case *types.Named:
for i := 0; i < typ.NumMethods(); i++ {
if m := typ.Method(i); token.IsExported(m.Name()) && isSupportedSig(m) {
methods = append(methods, m)
if len(methods) > limitFunctionCount {
break
}
}
}
case *types.Interface:
for i := 0; i < typ.NumMethods(); i++ {
if m := typ.Method(i); token.IsExported(m.Name()) && isSupportedSig(m) {
methods = append(methods, m)
if len(methods) > limitFunctionCount {
break
}
}
}
}
After the change:
switch typ := typ.(type) {
case *types.Named, *types.Interface:
for i := 0; i < typ.NumMethods(); i++ {
if m := typ.Method(i); token.IsExported(m.Name()) && isSupportedSig(m) {
methods = append(methods, m)
if len(methods) > limitFunctionCount {
break
}
}
}
}
Orthogonality: How does this change interact or overlap with existing features?
This should make go code easier to read and write.
Would this change make Go easier or harder to learn, and why?
Easier, I think the current behavior is non intuitive.
Cost Description
This requires someone to implement the transformation in the compiler. Also gopls needs to be updated to know that this is something the compiler does. And someone needs to update the language spec.
Changes to Go ToolChain
gopls, compile
Performance Costs
Some small compile time cost, no run time cost.
Prototype
This code:
switch typ := typ.(type) {
case *types.Named, *types.Interface:
for i := 0; i < typ.NumMethods(); i++ {
if m := typ.Method(i); token.IsExported(m.Name()) && isSupportedSig(m) {
methods = append(methods, m)
if len(methods) > limitFunctionCount {
break
}
}
}
}
Could be converted to:
switch typ := typ.(type) {
case *types.Named, *types.Interface:
{
typ := typ.(interface {
Method(i int) *types.Func
NumMethods() int
Underlying() types.Type
String() string
})
for i := 0; i < typ.NumMethods(); i++ {
if m := typ.Method(i); token.IsExported(m.Name()) && isSupportedSig(m) {
methods = append(methods, m)
if len(methods) > limitFunctionCount {
break
}
}
}
}
}