Enums, Go1 and proposals

436 views
Skip to first unread message

Nicolas Serna

unread,
Mar 3, 2024, 11:04:52 AMMar 3
to golang-nuts
Hello, gophers. Lately I've been working quite a bit with enums as I'm moving a C program I had to Go, so I wondered if there was any progress on enums proposals and realized that none of them get anywhere without breaking the Go1 compatibility.

I've decided to get a bit creative and share with you the proposals I've thought of.

The fundamental thing when we talk about the incorporation of "robust enums" is, similarly to the case of generics, to propose a syntax extension to the language without breaking compatibility with the previous software.

We use enumerations simply to handle states and encapsulate them under a name. Therefore I wanted to propose the following syntax:

```proposal
const (<ENUM_TYPE>) <NAME> <TYPE> = <VALUE>
```

The idea of this syntax is that, roughly speaking, it reminds us of what we do when we declare a method, with the only difference that in this case we use constant values associated to some "enum type". Then, we should be able to call our constants as follows: <ENUM_TYPE>.<CONSTANT_NAME>, the same would be true for an already instantiated type.

```example
type Statement struct{ /* ... */ }

type StatementTyp int

const (Statement) (
         Prepared StatementTyp = iota
         Success
         Fail
)

func main() {
        stmt := Statement{}
        fmt.Println(Statement.Prepared) // 0
        fmt.Println(stmt.Success) // 1
}
```

Realistically speaking this doesn't solve much. It just gives us a way to encapsulate constants in an "elegant" way and keeps the "const-iota" syntax, but little else.

I think it is essential to have an extra way to reference these internal values of a type so that we can work with the help of the go-typechecker. For this I could only come up with two types of syntax, let's see:

```prop1
var bar const <ENUM_TYPE>

func foo(arg const <ENUM_TYPE>) {...}
```
```prop2
var foo <ENUM_TYPE>.const

func bar(arg <ENUM_TYPE>.const)
```

That would be all, I hope you can give some feedback and know what you think. I read in the go2-language-template that it was better to post my ideas in this group instead of making a issue.

Thanks for reading ^^

Mike Schinkel

unread,
Mar 3, 2024, 5:32:24 PMMar 3
to Nicolas Serna, GoLang Nuts Mailing List
I have recently seen many are complaining about a lack of enums in Go.  But while there are many ways in which I would like to see Go improved, enums barely even rank on my list of priorities.

The majority of my experience prior to Go was with dynamic languages that did not have explicit enums, and in working with Go I never really felt that enum functionality was missing.

That said, what am I missing?  What it is about enums that have so many people clamoring for them in Go?  Is it having a textual representation managed by the compiler that a go:generate cannot solve?  Is it having a syntax that allows treating them like an object with a property, e.g. HttpStatuses.NotFound that otherwise requires too much boilerplate?  Or is it something else?

Further, what can you envision doing with a "proper" enum that you cannot already do with a custom-typed constant?

Thank you in advance to whoever helps me understand why enums are such as burning desire for so many developers.

-Mike

On Mar 3, 2024, at 12:25 AM, Nicolas Serna <serna.nic...@gmail.com> wrote:

Hello, gophers. Lately I've been working quite a bit with enums as I'm moving a C program I had tGo, so I wondered if there was any progress on enums proposals and realized that none of them get anywhere without breaking the Go1 compatibility.
--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/134fd31b-9081-4d22-b098-412244338fc5n%40googlegroups.com.

Jeremy French

unread,
Mar 4, 2024, 10:18:05 AMMar 4
to golang-nuts
What I find valuable is to be able to accept an enum as a parameter to a function, and know that it will be one of several approved values (e.g. month of the year), without having to have boiler plate to check or throw errors if it's not one of the approved values.  It's checked at compile-time rather than run time.  Whether that could be handled via go:generate or not, I wouldn't know.  I haven't used go:generate, but frankly at first glance it seems like using a sledgehammer to crack a walnut.  Maybe if you use go:generate regularly anyway, that would make sense.  But it seems like overkill to involve go:generate just to be able to have enums.  But again, my familiarity with go:generate is that which you get from 30 seconds of a google search.

Jan Mercl

unread,
Mar 4, 2024, 10:32:44 AMMar 4
to Jeremy French, golang-nuts
On Mon, Mar 4, 2024 at 4:19 PM Jeremy French <ibi...@gmail.com> wrote:

> It's checked at compile-time rather than run time.

That requires immutability of variables of enum type. Go does not
support immutable variables.

Brian Candler

unread,
Mar 4, 2024, 10:47:27 AMMar 4
to golang-nuts
On Monday 4 March 2024 at 15:18:05 UTC Jeremy French wrote:
What I find valuable is to be able to accept an enum as a parameter to a function, and know that it will be one of several approved values (e.g. month of the year)

ISTM all that's needed is to have a way to create a named type which cannot be assigned from the values of the underlying type, except inside a const declaration.

Does this represent the problem statement?

Jeremy French

unread,
Mar 4, 2024, 12:18:52 PMMar 4
to golang-nuts
More, to prevent PrintMonth(14), which the function would have to check for and either return an error or panic, since there is no meaningful output.  In fact, it's fairly easy to see, even in this case, where the PrintMonth signature would have to return an error, when that is pretty much the only case where it would need to.  With an enum as an input parameter, the PrintMonth() function would not need to have the error return value.  It's the same type of philosophy around interfaces.  If you accept as an input parameter an interface that has a PrintMonth() function, then you don't have to double check that the passed struct/object has a PrintMonth() function. You can just use it.  If you accept a Month enum as an input parameter, you don't have to check that it's between 1 and 12 inclusive.

BTW, this isn't something I'm passionate about.  I was more just answering Mike Schinkel's question about why it would be useful.

Jan Mercl

unread,
Mar 4, 2024, 12:40:23 PMMar 4
to Jeremy French, golang-nuts
On Mon, Mar 4, 2024 at 6:19 PM Jeremy French <ibi...@gmail.com> wrote:

> More, to prevent PrintMonth(14), which the function would have to check for and either return an error or panic, since there is no meaningful output. In fact, it's fairly easy to see, even in this case, where the PrintMonth signature would have to return an error, when that is pretty much the only case where it would need to. With an enum as an input parameter, the PrintMonth() function would not need to have the error return value. It's the same type of philosophy around interfaces. If you accept as an input parameter an interface that has a PrintMonth() function, then you don't have to double check that the passed struct/object has a PrintMonth() function. You can just use it.

The static type of an interface can be verified at compile time. The
value of a variable of an enum type (or a Pascal-like subrange) type,
cannot be verified at compile time in the general case. You would have
to add features Go does not have (like variable immutability) and/or
restrict some features for enum types that Go does have (like taking
address and using unsafe). Neither looks to me very probable, at least
in a short time.

Those values can be however checked at runtime, similarly to
array/slice index bounds checking. For subrange-like enums it's rather
cheap and can be often eliminated. General, sparse-valued enums are
commonly too expensive to check at runtime for the compiler. to insert
them automatically.

Ian Lance Taylor

unread,
Mar 4, 2024, 12:57:25 PMMar 4
to golang-nuts
On Mon, Mar 4, 2024 at 9:40 AM Jan Mercl <0xj...@gmail.com> wrote:
>
> The static type of an interface can be verified at compile time. The
> value of a variable of an enum type (or a Pascal-like subrange) type,
> cannot be verified at compile time in the general case. You would have
> to add features Go does not have (like variable immutability) and/or
> restrict some features for enum types that Go does have (like taking
> address and using unsafe). Neither looks to me very probable, at least
> in a short time.
>
> Those values can be however checked at runtime, similarly to
> array/slice index bounds checking. For subrange-like enums it's rather
> cheap and can be often eliminated. General, sparse-valued enums are
> commonly too expensive to check at runtime for the compiler. to insert
> them automatically.

In order to use runtime checks reliably, then for something like an
enum field in a struct where we have a pointer to the struct, we would
have to validate the enum value each time it is used. And presumably
panic if the value is out of range. This is doable but it's hard for
me to believe that this is really what people want when they say that
they want enums. In particular it's not how enums work in C/C++. It
is arguably how enums work in Java and Python but since they don't
have pointers the situation is somewhat different. Rust enums are
significantly different in that each enum value can have its own
individual attached data, making them more like tagged union types.

I think the biggest reason that we haven't added enums to Go, beyond
the predeclared constant iota, is that there is no consensus as to
what people actually want. I'm not saying that if there was consensus
that we would add enum types. But I do think it's very unlikely that
we would add enum types in the absence of consensus.

Ian

Nicolas Serna

unread,
Mar 4, 2024, 1:28:42 PMMar 4
to golang-nuts
I think what Jeremy mentions in here is the key to the issue. There would be more consensus and less interest in "fixing" enums in Go if there was a tangible way to restrict their values to what is stated by the developer.

Mike Schinkel

unread,
Mar 4, 2024, 2:32:11 PMMar 4
to Jeremy French, GoLang Nuts Mailing List
On Mar 4, 2024, at 12:18 PM, Jeremy French <ibi...@gmail.com> wrote:

More, to prevent PrintMonth(14) which the function would have to check for and either return an error or panic, since there is no meaningful output. ...  I was more just answering Mike Schinkel's question about why it would be useful.

Thank you for answering my question.

I tried to come up with a solution to that problem in Go using existing features and — assuming we want our enum-like values to be immutable and we don't want write a large amount of boilerplate — I was not successful.

On Monday, March 4, 2024 at 10:47:27 AM UTC-5 Brian Candler wrote:
Does this represent the problem statement?

I'd like to present my simple analysis of the catch-22 and see if others concur?

1. In order to get immutability without significant boilerplate, we need to use `const`.

2. We *can* define a type like `Month` as Brian Candler's example showed.

3. We also can assign values of our type to our `const` constants.

4. However, we can *only* assign literals or simple expressions to a `const`. i.e. we cannot assign `Month{1}` to a `const` if `Month` were a `struct`, `map`, `array`, or `slice`.

5. Further, funcs allow literals to be used as a type without requiring them to be cast as that type which is why `PrintMonth(14)` is both possible and problematic.

6. All values that we can validly assigned to a `const` are literal values that can also stand-in for the type when passed to a func that expects that type as a parameter.

Does that summarize our quandary correctly?  Did I leave anything out?

Assuming I did summarized correctly then it seems that any one or more of these potentially simple language enhancements would suffice to address the stated concern?

1. Allow a `const` to somehow be defined as `strict` where `strict` is a placeholder for the concept; i.e. that  `PrintMonth(14)` would not be possible but  `PrintMonth(April)` would be.

2. Allow a `func` too somehow have its parameter defined as `strict`, i.e. that again, `PrintMonth(14)` would not be possible but  `PrintMonth(April)` would be.

3. Allow a `var` to somehow be defined as immutable thus allowing immutable vars and 
types of subtypes `struct`, `map`, array, and/or slices to be used as pseudo-enums.

4. Relax the allowable values that can be assigned to a `const` to include values that can be fully determined at compile time such as `const April = [1]byte{4}` thus allowing `const` and 
types of subtypes `struct`, `map`,  array, and/or slices to be used as pseudo-enums.

Did I miss any potentials?  Did I get anything wrong? 


-Mike

P.S. Seems to me that #4 might be the simplest way forward as it would allow for creation of types that cannot be represented as their literal subtype value to pass as a parameter to a `func`, and it would not require any new keywords or concepts added to Go the language, or at least I don't think it would? 

Is there any reason Go could not relax the allowable values for const to be able to support constructs that can be determined at runtime such as `const April = [1]byte{4}`?

As an aside, if this solution is viable then it would also be nice if the cast `Month(4)` could translate to `[1]byte{4}` when `Month` is defined as `[1]byte`, and equivalent for `struct` and slices, i.e. that `Month(4) would assign to the first element or property of the `struct`, array, or slice. It could easily be limited to those with only one element or property, though.

Ian Lance Taylor

unread,
Mar 4, 2024, 5:15:44 PMMar 4
to Mike Schinkel, Jeremy French, GoLang Nuts Mailing List
On Mon, Mar 4, 2024 at 11:32 AM Mike Schinkel <mi...@newclarity.net> wrote:
>

...

> 4. Relax the allowable values that can be assigned to a `const` to include values that can be fully determined at compile time such as `const April = [1]byte{4}` thus allowing `const` and
> types of subtypes `struct`, `map`, array, and/or slices to be used as pseudo-enums.

...

> P.S. Seems to me that #4 might be the simplest way forward as it would allow for creation of types that cannot be represented as their literal subtype value to pass as a parameter to a `func`, and it would not require any new keywords or concepts added to Go the language, or at least I don't think it would?
>
> Is there any reason Go could not relax the allowable values for const to be able to support constructs that can be determined at runtime such as `const April = [1]byte{4}`?

I think this is https://go.dev/issue/6386.

There are subtleties. The simple version only works if the variable
can be declared and initialized in the same statement. That isn't
alway feasible, which is why init functions exist. It absolutely
prohibits circular data structures, which could be limiting. But
anything else requires some way to separate the declaration from the
initialization while still prohibiting the value from changing after
initialization.

Ian

Mike Schinkel

unread,
Mar 4, 2024, 7:01:06 PMMar 4
to Ian Lance Taylor, GoLang Nuts Mailing List
On Mar 4, 2024, at 5:14 PM, Ian Lance Taylor <ia...@golang.org> wrote:
P.S. Seems to me that #4 might be the simplest way forward as it would allow for creation of types that cannot be represented as their literal subtype value to pass as a parameter to a `func`, and it would not require any new keywords or concepts added to Go the language, or at least I don't think it would?

Is there any reason Go could not relax the allowable values for const to be able to support constructs that can be determined at runtime such as `const April = [1]byte{4}`?

I think this is https://go.dev/issue/6386.

Thank you Ian for acknowledging, and for the link, and yes that issue does cover it.

However, that issue is a general superset whereas I was proposed a very constraint language enhancement.

In that issue it seems the pushback was:

1. It is unclear where to draw the line on what to allow because of all the ramifications, and 

2. "Nice to have" is not sufficient reason to add a feature to the language.

The simple version only works if the variable
can be declared and initialized in the same statement.  That isn't
alway feasible, which is why init functions exist.  It absolutely
prohibits circular data structures, which could be limiting. 

Those are definitely challenging concerns when considering the larger ask for the proposal in issue 6386.

However, to address the enum use-case maybe a much smaller subset proposal could suffice, one that can be declared and initialized in the same statement and that has no need for circular data structures.  

One that could also address both of those aforementioned criticisms:

1. The subset to include would only be those things that would be needed to address the enum use-case, which I think would be fixed-sized arrays and structs (and not maps nor slices I previously pondered.) 

Those should be able to contain only other constants, which I think would include contained fixed-sized arrays and structs, but nothing with a pointer and attempting to take the address of any of those constants should throw a compiler error.

2. This proposal would not be because it is "nice to have" but instead it would enable a developer to create function whose parameters could only be satisfied by one of the values of a type used for enums as specified by the developer.  IOW, it solves a legitimate development concern and is not just a nice-to-have.

The only real downside I can see is if implementing this would someone paint the language in a corner so that it could not be enhanced in a better way in the future. Maybe my vision is too limited, but that doesn't seem likely with such a small, backward compatible change that introduces nothing new other than relaxing an existing constraint.

Certainly some people would say "Well if you are going to do that, why not do the entirety of 6386?" And if one of my hot buttons required arbitrary types to be assigned to a const, I might be one of those people too. But the Go team seems to be particularly adept at saying "We are doing this because we have a specific reason, and today, we are only doing this. Maybe we'll do more in the future, but not today." Maybe that could allow improves enums in the next version of Go without having to eat the elephant all at once?

Here I modified is Brian Candler's example to be what I envision could address the enum use-case:

1. Non-pointer instances of `struct` and `[...]array` are assignable to a `const`, and 
2. Casting a literal to a `struct` `[...]array` type would instantiate the type and assign the literal's value to the first element or property.


Maybe this is a sufficiently small proposal to address this valid use-case that the Go team could get behind it?

-Mike


Reply all
Reply to author
Forward
0 new messages