-
Notifications
You must be signed in to change notification settings - Fork 18.3k
Description
-
Would you consider yourself a novice, intermediate, or experienced Go programmer?
Intermediate, multiple projects, plenty of learning -
What other languages do you have experience with?
JS/TS, Swift, Python, C#, C++ -
Has this idea, or one like it, been proposed before?
Yes, the enums are a popular under proposal subject matter. -
If so, how does this proposal differ?
It doubles down on the status quo (aka historically idiomatic) solution for enums in Go. All current Go enum declarations will remain valid. Instead of introducing new keywords or interfering with existing patterns (such as the Stringer interface) it adds tools for enforcement and runtime-safety of enumeration usage. It does so by employing existing type assertion and type casting idioms, and theconst
keyword idiomatic to Go enum declarations. -
Who does this proposal help, and why?
For those adopting enums in Go, it is important to limit their use to the values known at compile time, or explicitly opt-in to the type-casting and therefore verifying the primitive values. The new syntax will not eliminate the need for authors of packages exposing an enumeration type to create checks on enum usage. Rather, this proposal will clarify the party responsible for enum value validation and provide tools that will make that validation idiomatic, clearly identifiable and easy to implement. -
Is this change backward compatible?
Yes. -
Show example code before and after the change.
I used http package as an example but I do not advocate redeclaring standard library constants as enums.
Beforeconst ( MethodGet = "GET" // ... ) func NewRequest(method, url string, body io.Reader) (*Request, error) { switch method { case MethodGet: // ... default: return nil, ErrUnknownMethod } }
After (please see comment below for an evolved, but still work in progress update to the proposed changes)
type Method string const ( MethodGet Method = "GET" // ... ) // NewRequest with original signature allows for freeform string method func NewRequest(method, url string, body io.Reader) (*Request, error) { // method, ok := method.(const) // ERR: can't check untyped string value against known constants // but if the user provides a set of constants to check, we can use a switch: switch method.(const) { case MethodGet: // ... default: // The default case in a .(const) switch is a requirement return nil, ErrUnknownMethod } // Or we can cast to a named type, effectively switching over cases for each of enum's constant values method, ok := Method(method).(const) if (!ok) { return nil, ErrUnknownMethod } // Now that method is a known constant, the .(const) switch may drop its default clause // ... } // NewRequestEnum requires enum type but has no opinion on its source // For the caller, it makes it clear the first argument is unlike the second. func NewRequestEnum(m Method, url string, body io.Reader) (*Request, error) { // ... } // NewRequestConst is not callable from if using Method cast from a primitive type without performing .(const) check. // For the caller, it makes it clear there is a set of values of named type already provided. func NewRequestConst(m const Method, url string, body io.Reader) (*Request, error) { // This is a "MAYBE??": addition to the syntax, borrowing from "chan type", // "const type" requires caller to use a pre-defined constant value // or use switch.(const) and .(const) type-cast to fulfill that obligation // when sourcing from a primitive type. // See comment below for me pulling back from this. }
-
What is the cost of this proposal? (Every language change has a cost).
The new syntax clashes with.(type)
and theconst
addition to function signature argument is opening a can of worms. -
How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
New syntax would depend on linting and formatting to ensure ease of adoption. -
What is the compile time cost?
Compiler would need to identify all references to a namedconst
type and ensure the binary has type-specific facilities for executing.(const)
checks. Additionally, when a function signature (or perhaps even a struct field) requiresconst
argument, it must identify the call site and ensure the.(const)
conversion takes place prior to call. -
What is the run time cost?
During program execution the new.(const)
checks are done against switch cases or statically instanced collections of pre-defined values. If the type casting for enum values is present in the binary, and the new syntax is employed unsafely (e.g. the.(const)
casting omitsok
variable), thepanic
messaging must clearly identify the mismatch between available pre-defined enum constants and the result of the runtime-cast of from a primitive type. -
Can you describe a possible implementation?
No. I don't have a familiarity with the Go source code. Point me in the right direction and I would love to contribute. -
Do you have a prototype? (This is not required.)
No, but willing to collaborate. -
How would the language spec change?
I don't have an off-hand grasp of EBNF notation, but in layman's terms the spec will extend type assertion, type switch, and function parameter usage to make possible the new syntax. -
Orthogonality: how does this change interact or overlap with existing features?
It makes use of type assertion and type casting syntax as the basis. -
Is the goal of this change a performance improvement?
No. -
Does this affect error handling?
Yes, the advancement of enum support in Go through this or another proposal must ensure more elaborate analysis at exception site. In case of this proposal, it is now possible to identify -
If so, how does this differ from previous error handling proposals?
I don't know. -
Is this about generics?
No.
Original ideas and research
Prior Art
In no specific order:
- proposal: Go 2: iota for strings (useful for enums) #32176 has shown there is some confusion behind the sense of enum not being a native? type (sugar), but an engineering pattern/trick.
- There are plenty of examples of this pattern being baked into the language, some outlined in proposal: spec: enums as an extension to types #28987. (As well as clear argumentation of what needs to improve.)
- In fact
case
keyword has already been appropriated for enum declarations in proposal: spec: enum type (revisited) #28438 leading to defacto parity with language like Swift. - While others, e.g. proposal: spec: enum type (revisited) #28438 need enums to be akin structs with dot-notation hierarchy access as if it's a graph.
- Lacking Range type sugar is another source of confusion (proposal: Go 2: range types #30428 and).
- Some range issues are really about constraining the enumeration — proposal: Go 2: new type: integer range constraint #29649
- Aliasing possibility (spec: clarify meaning of alias decls in sequence of "enum" const declaration #17784) for a short time further confused the enum declarations.
- Apparently, clarity of Go int enumeration being an ordered list of things is a feature, and go/printer: consider permitting one-line "enum" const decls #13403 seems like it would be an unnecessary change to the
gofmt
status quo. - There is enum chatter which is really about getting Strings out of the enum values (proposal: Go 2: iota for strings (useful for enums) #32176, x/tools/cmd/stringer: Generate a method to get the enum value (int) form its name (string) #13744).
- Interestingly enough the immutability question, which underlies the enumeration pattern, has been discussed in proposal: spec: immutable type qualifier #27975. @beoran cites people saying "go doesn't have enums", when they mean "go doesn't implement conveniences based on enum keyword"
iota
usage extension has been mentioned in proposal: permit iota, omission of init expressions in var declarations #21473 and I take it as a signiota
will not be going away...- The root proposal: spec: add typed enum support #19814 is again about keyword introduction to purge the unexpected conditions in a "enum" value switch. The earlier questions Why are there no enums in Go? #17205, spec: enum does not enforce type check under certain circumstances #9481 outline the core needs — how do we ensure only known values are accepted?
- Ultimately, x/tools/cmd/vet: complain about incomplete switch statements #5469 decided not to touch the enum switching, but Go 2 seems like a great time to revisit.
- Very early on (add support for const struct values #359) the type-safety was the only omission from the const-enum.
Thoughts
-
enum
is indeed new type, which is whattype State string
does, there is no idiomatic need to introduce a new keyword. Go isn't about saving space in your source code, it is about readability, clarity of purpose. -
Lack of type safety, confusing the new
string
- orint
-based types for actual strings/ints is the key hurdle. All enum clauses are declared asconst
, which creates a set of known values compiler can check against. -
Stringer
interface is the idiom for representing any type as human-readable text. Without customization,type ContextKey string
enums this is the string value, and foriota
-generated enums it's the integer, much like XHR ReadyState codes (0 - unsent, 4 - done) in JavaScript.Rather, the problem lies with the fallibility of custom
func (k ContextKey) String() string
implementation, which is usually done using a switch that must contain every known enum clause constant. -
In a language like Swift, there is a notion of an exhaustive switch. This is a good approach for both the type checking against a set of
const
s and building an idiomatic way to invoke that check. TheString()
function, being a common necessity, is a great case for implementation.
Proposal
package main
import (
"context"
"strconv"
"fmt"
"os"
)
// State is an enum of known system states.
type DeepThoughtState int
// One of known system states.
const (
Unknown DeepThoughtState = iota
Init
Working
Paused
ShutDown
)
// String returns a human-readable description of the State.
//
// It switches over const State values and if called on
// variable of type State it will fall through to a default
// system representation of State as a string (string of integer
// will be just digits).
func (s DeepThoughtState) String() string {
// NEW: Switch only over const values for State
switch s.(const) {
case Unknown:
return fmt.Printf("%d - the state of the system is not yet known", Unknown)
case Init:
return fmt.Printf("%d - the system is initializing", Init)
} // ERR: const switch must be exhaustive; add all cases or `default` clause
// ERR: no return at the end of the function (switch is not exhaustive)
}
// RegisterState allows changing the state
func RegisterState(ctx context.Context, state string) (interface{}, error) {
next, err := strconv.ParseInt(state, 10, 32)
if err != nil {
return nil, err
}
nextState := DeepThoughtState(next)
fmt.Printf("RegisterState=%s\n", nextState) // naive logging
// NEW: Check dynamically if variable is a known constant
if st, ok := nextState.(const); ok {
// TODO: Persist new state
return st, nil
} else {
return nil, fmt.Errorf("unknown state %d, new state must be one of known integers", nextState)
}
}
func main() {
_, err := RegisterState(context.Background(), "42")
if err != nil {
fmt.Println("error", err)
os.Exit(1)
}
os.Exit(0)
return
}
P.S. Associated values in Swift enums are one of my favorite gimmicks. In Go there is no place for them. If you want to have a value next to your enum data — use a strongly typed struct
wrapping the two.
Originally posted by @ermik in #19814 (comment)