10 ms·
Safer Enums in Go
- cletus 4y agoI like the simplicity and how opinionated Go is. There's typically one way to do things and that's incredibly useful. But lack of real enums for me falls into the "How is this a thing?" bucket. Like Java introduced what are (IMHO) a pretty good enum almost 20 years ago. Java enums are classes, basically. They can have state and methods. Super-useful. I like Hack but Hack's enums are a little weird in the sense that you have a bit of an odd syntax to indicate if automatic coercion is allowed. I like the idea in this post as being better than straight strings or ints but there are still 2 downsides: 1. As a variable (not a constant) it gives the compiler less options on how this can be optimized; and 2. (This is the big one) One of the best things about enums is using them in switch statements such that adding a new value will cause a compiler error (assuming you don't use a default branch, which should be discouraged for this). Rust enums and match expressions, for example, cover all of this.
- cube2222 4y agoNot that it's a great solution, but there's https://github.com/BurntSushi/go-sumtype https://github.com/BurntSushi/go-sumtype for (2).
- majewsky 4y agoThese sumtypes don't mesh well with JSON/YAML/etc. serialization though. If you have type MySumType interface { sealed() } like in the code example for go-sumtype, serialization is easy enough: type MySumType interface { json.Marshaler sealed() } But deserialization runs into a roadblock, because you cannot implement interfaces like json.Unmarshaler on an interface type. Something like type MySumType interface { json.Marshaler json.Unmarshaler sealed() } will not work: Given `var s MySumType`, json.Unmarshal() will croak because it cannot call UnmarshalJSON without knowing the concrete type of `s`. So for deserialization, you'd need an extra wrapper type that provides the json.Unmarshaler implementation.
- catlifeonmars 4y ago> But deserialization runs into a roadblock, because you cannot implement interfaces like json.Unmarshaler on an interface type. In addition to introducing the wrapper type, this could also be fixed by using a JSON decoder implementation that supports type/location hooks. In other words, this is more of a limitation of the custom unmarshaling mechanism chosen by the builtin encoding/json package.
- masklinn 4y ago> So for deserialization, you'd need an extra wrapper type that provides the json.Unmarshaler implementation. You'll probably need something along those lines either way, for the simple reason that there is no single mapping between sum types and JSON (or equivalent / similar) For instance serde provides 3 different serialisation schemes, and once in a while you still need a bespoke deserialiser.
- Splizard 4y agoRight, the builtin switch statement is not exhaustive but you can quite easily create a Switch method on your enum type in Go 1.18+ that is exhaustive.
- mieubrisse 4y agoWe tried out struct-based enums like this for a while and they're definitely better than the vanilla version, but they still have the problem of "how do you iterate over all the values?" and "how do you create an enum value from a string?". We chanced upon https://github.com/dmarkham/enumer https://github.com/dmarkham/enumer a few months ago and have been very happy so far.
- mariusor 4y agoI think sometimes it's important to accept the shortcomings of the language we're using instead of chasing high overhead abstractions like the one presented in the OP and in the enumer package. I hate that go doesn't have enums, I hate that it doesn't have tagged structs, but if I spend my time trying to massage the language into simulacra of them, I'm losing time that I could spend writing productive code, or I muddle the waters for future developers that will have to deal with those abstractions in the future. Overall I think it's not worth it, outside of highly theoretical implementations like this, that can exist independent of code that actually does things.
- mssdvd 4y agoHow come the authors did not want to add proper sum-types?
- deleted 4y ago[deleted]
- masklinn 4y agoBecause they ignored essentially everything we learned after 1990 if not 1980.
- vocram 4y ago> The one tiny drawback is that the globals have to be variables instead of constants, but that’s one of those problems that really only exists in the theoretical realm. I’ve never seen a bug from someone overwriting a global variable like this, that is intended to be immutable. Even if it’s a silly bug to make, it really annoys me a lot knowing you could change an enum value. Is it so hard for Go to support consts of any-type?
- ebingdom 4y agoAlthough an improvement over C, Go seems to have the same unofficial motto: "I don't need you to X, just trust me to Y", as in: 1) I don't need you to prevent me from mutating this variable, just trust me to not mutate it. 2) I don't need you to ensure I handle every case of this enum, just trust me to update all the relevant code whenever I add a new case. 3) I don't need you to prevent me from reading the result without checking the error, just trust me to always handle errors. 4) I don't need you to track nullability in the type system, just trust me not to dereference null pointers. 5) I don't need you to let me hide the default constructor, just trust me to always use the smart constructor that establishes the invariants that I need. 6) Until recently: I don't need you to let me abstract away this type, just trust me to keep all the copies of the function/type in sync. This philosophy may make sense for a low-level language like C, but not for a high-level language for building applications, services, etc. It amazes me that people voluntarily give up the guarantees of other languages in favor of Go for these use cases. It's almost as if the language wants bugs to slip into your code.
- masklinn 4y ago> This philosophy may make sense for a low-level language like C Even for a low level language like C it really doesn't, it makes the implementation easier but all the things you mention (and more) are useful both to ensure the foundations are solid (security, correctness) and to make it fast: the weak type system and feeble guarantees of C are why compilers lean so much on UBs to infer constraints they can then optimise based on.
- cesnja 4y ago
- donatj 4y ago> The one tiny drawback is that the globals have to be variables instead of constants, but that’s one of those problems that really only exists in the theoretical realm. I’ve never seen a bug from someone overwriting a global variable like this Same goes for the original problem. I’ve heard a lot of people complain about Go’s enum types accepting hard coded values, it’s a conceptual problem, sure, but is it a real problem in practice? It’s not something I would ever do by accident. I don’t find my self passing random hard coded values to methods. I would have to look up the actual expected enum value, at which point I would think “I should make this a constant” at which point it fails to compile and I find the enum. Even then, in reality I would have noticed the type and the enum long before that. Hard coded values in and of themselves are a code smell. Like sure, in theory a typo could compile, but it seems like a hard mistake for a non-malicious developer to make.
- edumucelli 4y agoSimplicity is one thing, but lacking things such as Enums is just plain bad. Is this a programming language from the 70's
- ebingdom 4y agoI'd say it just as you did but with "algebraic data types" instead of "enums".
- xienze 4y ago> The one tiny drawback is that the globals have to be variables instead of constants, but that’s one of those problems that really only exists in the theoretical realm. I’ve never seen a bug from someone overwriting a global variable like this, that is intended to be immutable. Eh yeah, but if you’re writing a library you have to assume someone will do it, because it’s possible to do. Which makes it a non-starter IMO.
- bravogamma 4y agoC++ programmer here. I was about to start a deep dive into Go today. Hearing there are no enums is giving me serious pause. How do large systems (e.g. Docker?) deal with this?
- randomdata 4y agoIt has enums, but they are C-style enums rather than sum types. Which stands to reason given that C and Go were born to the same father.
- masklinn 4y ago> It has enums, but they are C-style enums rather than sum types. It doesn't. The closest it has is TFA's second example: type FlagID int const ( FooBar FlagID = iota FizzBuzz ) which is the underlying behaviour of C enums, but not actually that. Crucially it does not at any point hint or imply the set could be in any way closed. That it doesn't have C-style enums is a point in its favour, I would say. Not much of a point, mind, but still...
- randomdata 4y ago> It doesn't. It does. By definition, an enum is simply a set of named values, which you code example provides, and is behaviourally the same as C. > it does not at any point hint or imply the set could be in any way closed. While true, that is a feature of sum types, which I already indicated Go does not have. This is slightly different to enums.
- morelisp 4y agoIn C the enum declaration is - by idiom and also syntax - contained a single declaration. You can construct values outside those, but as a default assumption the compiler/linter has a reasonable list of values to claim "these are the enum values which need handling". In Go the values are not in a single declaration, and not even in a single TU. The strongest hint that this is an enum and not some other kind of integral newtype is the use of `iota` on the first declaration, but `iota` is also used for other purposes. You're technically correct that they're "behaviorally the same", but declaration structures matter a lot.
- vips7L 4y agoJust another poor language choice by the designers.
- tempest_ 4y agoHaving learned some Rust in the last few years (coming from python) I now want sum types everywhere. Anything less is a disappointment.
- alkonaut 4y agoThe C# of my day job often feels like banging rocks together with its c-style enums and lack of sum types. But there is always an article about go that makes you appreciate what you do have.
- throwaway858 4y agoC# has sum types
- masklinn 4y agoC# does not have sum types. It has "enumeration types" but their behaviour is exactly that of C's, with various additional conveniences but critically (and unlike Java for instance) no more type-safety.
- alkonaut 4y agoNot without squinting really hard and making some OO monstrosity with internal impls of an outer abstract type it doesn’t. And that’s not really language support it’s banging your rocks together really hard trying to bend it into being ML.
- munchler 4y ago> The C# of my day job often feels like banging rocks together with its c-style enums and lack of sum types. Hah, I agree. I switched over to F# and never looked back. It plays nicely enough with C# that my colleagues couldn't stop me (but didn't join me either). > But there is always an article about go that makes you appreciate what you do have. Yes. Go seems like a time machine back to about 1985.
- 4y ago
- titzer 4y agoVirgil has enums where each enum value can have additional fields, so you can write tables and name individual rows. Under the hood the enum value is just an integer and "accessing a field" is just an access of a constant in an array. It's turned out to be incredibly useful. https://github.com/titzer/virgil/blob/master/doc/tutorial/Enums.md https://github.com/titzer/virgil/blob/master/doc/tutorial/En...
- parhamn 4y agoI hope at some point golang drops its "too complicated" defense against doing more sensible things that aren't _that_ complicated but add a lot of safety. I say this as someone who loves go and has written A LOT of it. More and more people will use Rust and Typescript and realize what a joke go's type system is compared to those. But you don't need to go as far as those languages have -- lacking sum types, exhaustive switches, discriminant unions, proper enums, unchecked nils, etc is just lazy. Most reasonably sized software projects end up having some ad-hock crappy workaround to any one of these issues. After years of waiting for generics. We got the most stunted and simplistic implementation of generics imaginable. You can't even do something as simple function chaining.
- morelisp 4y ago> You can't even do something as simple function chaining. You make this sound like something got lazy about rather than something with major implications for the language semantics/runtime. Serious question: Would you prefer a) the current generics implementation, b) a runtime capable of code generation, c) generics cannot participate in interface implementation, d) some other option?
- suremarc 4y agoAm I the only one missing in the last example that the zero value, FlagID{}, would be an invalid enum? In which case this is not fundamentally different from the other cases, where you always have to check for the possibility of an invalid value.
- henvic 4y ago> The one tiny drawback is that the globals have to be variables instead of constants, but that’s one of those problems that really only exists in the theoretical realm. I'd argue that needing this enums safety so bad that you go out of your way to create a struct with a protected field also lies in the same category of something you need that is mostly in the theoretical realm too. Don't get me wrong, sometimes you want that kind of protection indeed and it's easy to mess up if you need to add validation everywhere. However, most times you can get away with just rethinking your code. More importantly, I think the downside of using variables rather than constants makes it less safe than the alternative of relying on you always having to do validation.
- vlunkr 4y agoI had the same thought. You're just swapping one non-issue for another.
- tedunangst 4y agoThe alternative of using plain strings is prone to typos, which variable names are not (or less so).
- henvic 4y agouh? I'm saying "just use regular constants". The risk of typos is when you're defining them, just as much as the risk of defining bad variable values.
- assbuttbuttass 4y agoI can understand the theoretical arguments for sum types and closed enums, but I have never once been bitten in practice by this issue.
- sidlls 4y ago"I’ve never seen a bug from someone overwriting a global variable like this, that is intended to be immutable." It can cause headaches in test code (where developers try to override the package variable value for test cases, and forget to restore/change it back when done, etc., i.e. they engage in bad practices that I shouldn't have to catch in a PR). It can easily cause production applications to blow up unexpectedly when the variable is changed in a function and that mistake passes through all the tests and the PRs. This practice is one of the worst in the go community, in my opinion. The first time I saw these package level globals in go code I cringed. It is an absolutely terrible design decision to not have package level immutables that aren't PODs.
- catlifeonmars 4y ago> It is an absolutely terrible design decision to not have package level immutables that aren’t PODs. FWIW here is a proposal to expand const in Go: https://github.com/golang/go/issues/21130 https://github.com/golang/go/issues/21130
- lenkite 4y agoI prefer immediately initialized struct vars var Animal = struct { Cat string Dog string Dodo string } { "Cat", "Dog", "Dodo", }
- xaduha 4y agoDitto, surely this is better than what this blog post suggests? var Constants = struct { FooBar string FizzBuzz string } { "FooBar", "FizzBuzz", } If you must have type FlagID string then this should work also var Constants = struct { FooBar FlagID FizzBuzz FlagID } { "FooBar", "FizzBuzz", }
- atombender 4y agoThat's not a type. That's just some string constants. You cannot define a variable of type Animal, which is kind of the point of enums. Worse, they're not constants, they're mutable. So how is that different from this? var ( Cat = "Cat" Dog = "Dog" Dodo = "Dodo" )
- xaduha 4y agoIn my example it would be accessible as Constants.FooBar and it would be of a type FlagID just as required. What else do you need? Mutability was dismissed in the blog post too and if you're modifying something called Constants.FooBar it's kinda on you anyway.
- atombender 4y agoYour example does not use a type, so I fail to see how it's an equivalent example. What does your example give you aside from "namespacing" the values under a generic struct?
- xaduha 4y agoRead the whole comment, FlagID is there and function signature func IsEnabled(id FlagID) bool { would be satisfied just fine
- politician 4y agoI write a lot of Go. Adding enums nor debugging them has never been a big problem for me. The way that I tackle the problem is to use iota, and then to emit the numeric value into logs. Later, if I have some time to get fancy, I’ll implement fmt.Stringer using a switch statement. Sometimes, I’ll implement a ParseFoo function to convert the string representation into the typed representation. All of this work takes about 5 minutes, and no thought. That’s what we’re talking about here: ~5 minutes of effort per enum. IMO, you should be spending far more time thinking about which values to add to the enum than the mechanics of string support.
- HL33tibCe7 4y agoThose 5 minutes add up, and in my experience Go newcomers get confused about iota (and the frankly hideous way enums have to be written in general) quite a lot. One of Go’s best aspects is its readability, so it’s unfortunate that it misses the mark here IMO.
- politician 4y agoI think the most distinct enum types I've had in a project was maybe 20? At some point we need to realize that this is bikeshedding.
- Tozen 4y agoI'm really not sure why Go decided against a distinct enum type, instead of repurposing const and getting odd with iota. It comes off as a kind of unnecessary stubbornness, for what should have been easy to add to the language early on. If you look at one of the Go inspired languages, such as Vlang (that has the enum type), it really should not have become an issue. Now, with Go being advanced in age, it comes off as just odd. It becomes reminiscent of the generics debate, where there was such long denial of it being necessary, then suddenly it is understood it should have been done earlier.
- HL33tibCe7 4y agoThe fact that these workarounds are even necessary is completely absurd, and I’m saying that as a dyed-in-the-wool Golang fan
- tonfreed 4y agoI think I'd rather just sanity check an incoming const than trust developers with global state. All it takes is one idiot to mutate one of those vars and all sorts of hell can break loose.
- bborud 4y agoI would guess that the cost of not having enums in Go by now is far higher than any cost of adding them. It is one of those things that annoys Go developers on a nearly daily basis.