Repository: go
Issue: proposal: spec: exhaustive switching for enum type-safety 
Labels: LanguageChange,Proposal,LanguageChangeReview

- **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 the `const` 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._
    Before

    ```go
    const (
        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](https://github.com/golang/go/issues/36387#issuecomment-583637427) for an evolved, but still work in progress update to the proposed changes)
    ```go
    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 the `const` 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 named `const` type and ensure the binary has type-specific facilities for executing `.(const)` checks. Additionally, when a function signature (or perhaps even a struct field) requires `const` 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 omits `ok` variable), the `panic` 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.


<details><summary>Original ideas and research</summary>

### Prior Art
In no specific order:

- #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 #28987. (As well as clear argumentation of what needs to improve.)
- In fact `case` keyword has already been appropriated for enum declarations in #28438 leading to defacto parity with language like Swift.
- While others, e.g. #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 (#30428 and).
- Some _range_ issues are really about constraining the enumeration — #29649
- Aliasing possibility (#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 #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 (#32176, #13744). 
- Interestingly enough the _immutability_ question, which underlies the enumeration pattern, has been discussed in #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 #21473 and I take it as a sign `iota` will not be going away...
- The root #19814 is again about keyword introduction to purge the unexpected conditions in a "enum" value switch. The earlier questions #17205, #9481 outline the core needs — how do we ensure only known values are accepted?
- Ultimately, #5469 decided not to touch the enum switching, but Go 2 seems like a great time to revisit.
- Very early on (#359) the _type-safety_ was the only omission from the const-enum.

### Thoughts
1. `enum` *is indeed* new type, which is what `type 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.

2. Lack of type safety, confusing the new `string`- or `int`-based types for actual strings/ints is the key hurdle. All enum clauses are declared as `const`, which creates a set of known values compiler can check against.

3. `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 for `iota`-generated enums it's the integer, much like [XHR ReadyState codes](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState) (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.

4. 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. The `String()` function, being a common necessity, is a great case for implementation.

### Proposal

```go
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.
</details>

_Originally posted by @ermik in https://github.com/golang/go/issues/19814#issuecomment-551095842_