1. 32
  1. 19

    One thing I miss almost on a daily basis in Go are enums. Using const with iota is no proper replacement, it’s error prone and opaque. Also, you need a linter to check that all variants of the “enum” consts are covered in a switch-case expression. I am curious why they designed Go without support for enums. Was it because there is no support for union types (could not find any details about the design decision)?

    1. 8

      After getting comfy with enums in Swift and Rust, boy do I miss them anytime I write Go.

      1. 4

        GP is talking about C-like enums, not Rust’s ADT enums. I also miss ADT in most languages.

      2. 4

        I miss enums as well, the whole const & iota piece feels very magical and un-Go like in my opinion.

        1. 3

          To me it perfectly fits Go’s idea of making the language small. Instead of an enum which is a big-ish language feature focused on one thing, you get a tiny general-purpose iota primitive that you can use to make yourself a DIY enum or something else.

          It’s the same idea as having multiple return values (a general purpose primitive) instead of more specific features for error handling.

          1. 2

            enums are hardly a “big” language feature. They are simply essential. The number of enums I write in an average application is tremendous - status codes, loading states, visual states, domain concepts - all are naturally “one of a set of values” that the enum is a 50 year old solution for.

            Also, introducing iota is the same cost to the language as introducing an enum concept! Only iota is completely specific to Go, no one has ever seen a language construct used this way before, so it has even more cognitive overhead than just adding enums.

            “DIY enum” - that should be your clue. Go makes you write unnecessary code here. It’s a really bad choice.

            1. 1

              Enums can grow into a bigger feature, e.g. they can be exhaustive or non-exhaustive, have unique non-int types (and related conversions), used as sets/bitfields, tied to switch statements (with other enum features making exhaustiveness checks difficult). Rust has also popularized sum types as enums, but that needs pattern matching, and works best with generics. Unless you do a half-assed job that C did, and Go didn’t want to repeat, it can easily grow to a big feature tied to other big features.

              I’ve specifically compared this approach to error handling, because it’s also an essential feature, and yet it’s another DIY pattern in Go, and boilerplate it needs is a butt of jokes.

        2. 2

          iota is by far the most enraging feature in Go. They take such pride in simplicity in the language, and argued for 10 years over generics when everyone knows it’s necessary with static types. But - they added by far the most idiosyncratic feature I’ve ever seen in a language: iota. And, I watched a talk somewhere where Rob Pike said that was one of his most proud features!!

          The problem is, I don’t see Go adopting pattern matching, so enums will be less useful. But regular C-style enums would even be extremely appreciated. I agree, it’s my most missed feature when writing Go.

          1. 1

            I agree. What is also annoying is that iota is so opaque and can be used in various inconsistent ways. All the forms below are equal, but luckily I never saw the last one in production. Personally I just prefer a mapping table over iota, despite its runtime overhead.

            const (
            	A1 = iota
            	A2
            	A3
            )
            
            const (
            	B1 = iota
            	B2
            	B3 = iota
            )
            
            const (
            	C1 = iota
            	C2 = iota
            	C3 = iota
            )
            
            const (
            	D1 = 0
            	_
            	D3 = iota
            	D2 = iota - 2
            )
            
        3. 10

          This is a really weird hodgepodge of issues.

          Some of these have no need to wait for a 2.0, as they’re just stuff to add to the stdlib:

          • The templating stuff
          • Logging

          Others are highly subjective (the current semantics of range make sense to me…)

          Deterministic select is actively a bad idea. The current semantics are not that the winning branch is “undefined” – it is explicitly defined to be chosen uniformly at random. This is for a good reason: if you have something like:

          select {
          case x := <- busy:
              // ...
          case y := <-other:
              // ...
          }
          

          i.e. there are two channels, and on one of them sends are happening all the time, if that case is matched first the other case could easily starve if the choice were deterministic, since it would only be taken when the other case is not applicable. Choosing a branch at random introduces fairness, so you don’t have to worry about this case. This eliminates a class of bugs that could be really nasty to hunt down.

          The motivating example also seems highly misguided: even in the “correct” version, there’s still a TOC-TOU bug in both of the latter branches of the last top-level select, where you check if the done happend, it hasn’t, but then it does immediately before you act on that information. It’s hard to suggest what to do here since, it’s not clear why you want the done signal to interrupt the other two cases, but most of the reasons I can imagine there’s some other state that’s more persistent that you’d want to check, probably at the top of the loop rather than inside those select branches.

          having a shorthand for “try to receive, but don’t block if it’s not ready” would indeed be nice, but again there’s no need to wait for a 2.0; it seems like it wouldn’t be that hard to add this in a non-breaking update to the language.

          +1 for getting rid of mutable globals (esp. public ones).

          1. 2

            having a shorthand for “try to receive, but don’t block if it’s not ready” would indeed be nice, but again there’s no need to wait for a 2.0; it seems like it wouldn’t be that hard to add this in a non-breaking update to the language.

            It’s easily written in Go 1.18 and later with generics. https://gotipplay.golang.org/p/iTPN_Ikv6F4

            there’s likely to be a channel-related package introduced into the standard library that will include functionality such as this, but not until Go 1.19 at the earliest.

          2. 9

            One thing that Go has going (heh) for it is that it is small: there are relatively few concepts to learn and keep in your head, there’s generally one obvious way to do something, etc.

            Something that the Go authors have succeeded in doing is fighting feature creep, generics notwithstanding. It may not be everyone’s cup of tea, but there is something to be said for languages that are “simple” from a programmer’s perspective. Sure it might not solve every problem, but sometimes I don’t want to pay the extra cost to use a language that solves every problem.

            1. 7

              it is impossible for a library author to emit structured logs

              I’m not sure that I agree that this is a problem, per se. If a library is emitting structured logs, is it doing so to some global logging object (eww, gross, please no), or are you injecting one? In the case that you’re injecting one, it’s likely an interface type (even the author says a structured logging interface), so that callers can specify their own logging backend. This appears to be what the author is suggesting, and it’s what I’ve done in several of my own projects. It works and it’s not hard to write, but everyone defines their own, which is what the author is suggesting is the problem.

              The problem with generalizing the interface described in the post to all projects is that it does nothing to prevent people from injecting things that look like loggers, but are actually arbitrary observers with callbacks that perform actions, using type-unsafe methods. I’ve seen exactly this; developers notice that they need to know when something happens in a library, and the library allows you to provide a logger, so give it a logger that takes action when it logs out a particular line. This actually -does work- as a workaround, but you wind up in the precarious position that you’re not just dependent on the API surface of the library, but its internal control flow. Once you accept that such a thing is possible and over a long enough period of time and with a large enough number of consumer inevitable, you come to a new realization: allowing a consumer to inject a polymorphic logger whose type they define alters the very nature of logging; it is no longer a textual history of events that have transpired, but a control flow device. Within your library, if you change what you are logging out, is that a backwards-compatible change? Well, not really; a consumer may have injected a thing that takes action when a particular event is logged out. “But that consumer is at fault” sure, but you laid that trap for them.

              A library right now can define its own observer type as an interface and allow callers to provide an observer. The library can then trigger type-safe events on that observer that are specific to that library. Done this way, the true nature of this structure is revealed: a polymorphic object provided on which methods are invoked is not really a log writer per se, it is an observer; a log writer is just a more specific kind of observer. If all you want to provide is a thing that does logging, you have to write a function whose input is your chosen logger and whose output is the observer type for the library that you’re calling into, and voila. That’s a bit of a nuisance if most of the time you want to do that, but probably worth the cost, since now the interface injected into the library can be type-safe, and the library authors are much more cautious about changing which events are being fired to the provided observers when they make changes.

              I don’t think libraries should ever write any log messages at all, for the above reasons, but also because doing so means that the library author is defining the language used for that application. What if you speak one language, but the consumer of your library work at a place where they speak another language? Should all of their operators in addition to all of their developers be able to speak your language to consume your log? If you’re accepting an observer and triggering events on it, it’s up to the consumer to turn that into human-readable info, if they so desire. Done that way, the result has better type semantics, discourages library authors from changing emitted events in backwards-incompatible ways, and accommodates language difference between teams.

              1. 5

                Great article!

                From the article:

                A modern templating engine

                Short-circuit evaluation. Go’s templating language always evaluates an entire conditional in a clause, which makes for some really fun bugs (that again will not manifest until runtime.)

                This is coming in Go 1.18:

                text/template

                The and function no longer always evaluates all arguments; it stops evaluating arguments after the first argument that evaluates to false. Similarly, the or function now stops evaluating arguments after the first argument that evaluates to true. This makes a difference if any of the arguments is a function call.

                Source: https://tip.golang.org/doc/go1.18.

                1. [Comment removed by author]

                  1. 2

                    I deleted my reply which was shown twice.