The problem isn't so much the lack of enums, it's more that there is no way to do exhaustive pattern matching at compile time. I have seen production systems go down because someone added a variant to an 'enum' but failed to handle that new variant everywhere.
Yeah, it makes a huge difference whether this is the default, and that's not a Go thing, it's not a programming language thing, it's the whole of human existence.
Literacy is an example. We're used to a world where it's just normal that other humans can make marks and interpret our own marks as to meaning. A thousand years ago that would be uncommon, ten thousand years ago vanishingly rare.
I really celebrate tools which take an idea that everybody agrees is a good idea, and bake it in so that everybody just has that, rather than agreeing it's a good idea but, eh, I have other things to do right now.
And it annoys me when I see these good ideas but they're left as an option, quietly for a few people to say "Oh, that's good to see" and everybody else misses out, as if "Literacy" was an optional high school class most of your peers didn't take and now they can't fucking read or write.
Example: Git has a force push feature, necessarily we can (if we have rights) overwrite a completely unrelated branch state, given any state X, now the state is our state Y instead. This isn't the default, that part is fine... Git also has "force-with-lease". This is a much better feature. Force-with-lease says "I know the current state of this branch is X, but I want to overwrite it anyway". If we're wrong, and X is not (any longer perhaps) the current state, the push fails. But force-with-lease isn't the default.
[Edited to fix clumsy wording in the last sentence]
The overarching lesson of my career has been that people are fallible and processes that rely on humans to not make mistakes are bound to fail. Recently I've also been thinking about the importance of being able to reason "locally" about code, it might be the single most important property of a software system. "locally" typically means a function, class, or module, but I think it can also be applied to "horizontal" cases like this. For example, if you add an enum variant the compiler should guide you to reason about each place where the enum is no longer exhaustively matched.
In theory yes. But practically there are always locations where we can't match every case, so we either have to live with a warning or add a catch-all arm. And as soon as a catch-all arm exists, we are in "not checked any more" state, but with a compiler that is supposed to check for exhaustiveness. Which is way worse if the catch-all arm isn't a `panic("Unhandled match branch!")`.
Yes, you can counter that with GADTs and match against exhaustive subsets. But to ergonomically handle these cases, you need something like Pattern Synonyms or you drown in boilerplate.
I'm implementing a somewhat complex software in both Go and Rust.
The Rust version is bith shorter and more readable - and probably more efficient - thanks to Rust enums and Rust error handling. I don't understand why golang doesn't copy Rust here. The error handling in particular could be a very simple change.
I am not a huge fan of go:generate and similar projects. They add a level of unknown that goes against the core Golang design values.
> I am not a huge fan of go:generate and similar projects. They add a level of unknown that goes against the core Golang design values.
I'm not sure I would agree with that. go:generate is a core part of Go since v1.4 and the enum generators are the kind of thing that was intended. https://go.dev/blog/generate
That said, enums would be a welcome improvement to the language. But even then, I think go:generate has a place.
"add(ing) a level of unknown" is a very fair description from a code comprehensibility standpoint, since the go:generate directive is about running arbitrary executables.
I’ve found the enumer [0] library does the job of generating usable helpers without too much pain or any obvious downsides. The ability to generate JSON [un]marshallers is particularly handy.
Still, the lack of enums and enum/sum types remains by far my biggest gripe about Go.
The posted article mentions https://github.com/abice/go-enum, which does the same thing, no? It even generates the consts based on a list of values in a comment.
It does indeed seem to be similar, and viable option, with the difference being that the enum values are specified in a comment. You seem enthusiastic about that design decision/feature, howeever I am not sure about it, it scares me a little.
It's absolutely just a hunch and personal preference but I worry that keeping the enum value inputs in comments might not be great for new contributors and in terms of maintaining the codebase over time, so I think I prefer the approach taken by enumer.
Enums in Go are not good. Code generation just makes it worse. Both because code generation has a bad smell and because nobody can agree on how to do enums in Go so we just end up with lots of diverging solutions.
Go 2 needs to have more usable enums. And while I'm not a big fan of "adding more stuff" to languages, it wouldn't hurt Go to learn a couple of things from Rust.
I guess it depends on how much of a departure from the current language it would be and if that introduces complicating factors. It would make sense to seriously consider adding matching (like in Rust) but then you might want to consider more than just enums?
I'm not a language design expert, but I suspect there be worms in that there can.
The data structure for a token that makes most intuitive sense to me is a tagged union.
So I defined an „const iota“-style enum. Stuck it into a struct that has the appropriate fields to cover all the cases and it was fine.
Having some syntax sugar for tagged unions would be nice. Having exhaustiveness checks if you switch over them, could be useful in some cases.
But that’s not where my mental energy went at all.
Reading the bytes (runes) efficiently and correctly into the data structure however is the part that needs focus. Once the data is in shape, I‘m not „worried“ at all anymore. Sure a bit of extra support is nice, but also kind of superficial.
Also going further, dispatching on them is again never the tricky part. It’s handling the cases correctly and efficiently that has my focus.
In Clojure, a common thing is to write multimethods that dispatch on (namespaced) keywords. Similar in spirit and structure, but each method might now reside in a different namespace or not even be written by you. But I have never worried about exhaustive matching or similar. What’s in the method bodies is the important part.
I really miss value enums from Rust while working with Go, but overall I find Go more intuitive. Didn't know there was a way of auto-generating with stringer, so thanks for the information!
If there is one thing that I find missing is proper enum support in the Go language.
There are quite a few subtle caveats you can run into without proper support in the language like the article mentions. (E.g. Using iota, passing in an undefined enum)
This might be a very simple and ignorant thought, but I'd be fine if they completely worked the same as Rust's enum's. [if let] is such a powerful feature, along with match arms.
Having support from the language would be nice, though.
Literacy is an example. We're used to a world where it's just normal that other humans can make marks and interpret our own marks as to meaning. A thousand years ago that would be uncommon, ten thousand years ago vanishingly rare.
I really celebrate tools which take an idea that everybody agrees is a good idea, and bake it in so that everybody just has that, rather than agreeing it's a good idea but, eh, I have other things to do right now.
And it annoys me when I see these good ideas but they're left as an option, quietly for a few people to say "Oh, that's good to see" and everybody else misses out, as if "Literacy" was an optional high school class most of your peers didn't take and now they can't fucking read or write.
Example: Git has a force push feature, necessarily we can (if we have rights) overwrite a completely unrelated branch state, given any state X, now the state is our state Y instead. This isn't the default, that part is fine... Git also has "force-with-lease". This is a much better feature. Force-with-lease says "I know the current state of this branch is X, but I want to overwrite it anyway". If we're wrong, and X is not (any longer perhaps) the current state, the push fails. But force-with-lease isn't the default.
[Edited to fix clumsy wording in the last sentence]
Yes, you can counter that with GADTs and match against exhaustive subsets. But to ergonomically handle these cases, you need something like Pattern Synonyms or you drown in boilerplate.
The Rust version is bith shorter and more readable - and probably more efficient - thanks to Rust enums and Rust error handling. I don't understand why golang doesn't copy Rust here. The error handling in particular could be a very simple change.
I am not a huge fan of go:generate and similar projects. They add a level of unknown that goes against the core Golang design values.
I'm not sure I would agree with that. go:generate is a core part of Go since v1.4 and the enum generators are the kind of thing that was intended. https://go.dev/blog/generate
That said, enums would be a welcome improvement to the language. But even then, I think go:generate has a place.
Still, the lack of enums and enum/sum types remains by far my biggest gripe about Go.
[0] https://github.com/dmarkham/enumer
Deleted Comment
It's absolutely just a hunch and personal preference but I worry that keeping the enum value inputs in comments might not be great for new contributors and in terms of maintaining the codebase over time, so I think I prefer the approach taken by enumer.
https://github.com/search?q=enum+generator+language%3AGo&typ...
https://github.com/search?q=enum+generation+language%3AGo&ty...
Go 2 needs to have more usable enums. And while I'm not a big fan of "adding more stuff" to languages, it wouldn't hurt Go to learn a couple of things from Rust.
I'm not a language design expert, but I suspect there be worms in that there can.
The data structure for a token that makes most intuitive sense to me is a tagged union.
So I defined an „const iota“-style enum. Stuck it into a struct that has the appropriate fields to cover all the cases and it was fine.
Having some syntax sugar for tagged unions would be nice. Having exhaustiveness checks if you switch over them, could be useful in some cases.
But that’s not where my mental energy went at all.
Reading the bytes (runes) efficiently and correctly into the data structure however is the part that needs focus. Once the data is in shape, I‘m not „worried“ at all anymore. Sure a bit of extra support is nice, but also kind of superficial.
Also going further, dispatching on them is again never the tricky part. It’s handling the cases correctly and efficiently that has my focus.
In Clojure, a common thing is to write multimethods that dispatch on (namespaced) keywords. Similar in spirit and structure, but each method might now reside in a different namespace or not even be written by you. But I have never worried about exhaustive matching or similar. What’s in the method bodies is the important part.
https://pkg.go.dev/structs@master
What more sane languages would use attributes for, Go 1.23 will do it like this,
Lovely design.There are quite a few subtle caveats you can run into without proper support in the language like the article mentions. (E.g. Using iota, passing in an undefined enum)