Relearning Go: Day 6
Enums to Select
Enums
Enumerated types (enums) are a special case of sum types. An enum is a type that has a fixed number of possible values, each with a distinct name. Go doesn’t have an enum type as a distinct language feature, but enums are simple to implement using existing language idioms.
Enums in Go are constants with a tiny amount of type safety and a few “conveniences”, like:
- The
iota
keyword. This adds no type safety, obfuscates the values of constants, and makes non-local use brittle and fraught. For example, when reading/writing a constant to a database, the meaning of value 6, say, is not readily apparent nor grep’able, and a change to reorder, add, or remove a constant could cause unintended changes to the value of many others, all largely unobserved. - The stringer tool. Because Go is so simple that you need an out-of-band tool to write it for you.
Long before Go and after and up to the present day, many languages have and continue to demonstrate the extraordinary usefulness of sum types as a language feature. It seems like a colossal miss that Go did not and still does not have anything comparable.
Struct Embedding
gobyexample.com/struct-embedding
Starts off as a way to nest structs, then becomes a way to do something like multiple inheritance:
Since
container
embedsbase
, the methods ofbase
also become methods of acontainer
.
Embedding structs with methods may be used to bestow interface implementations onto other structs.
Go By Examples is a brisk walk through Go’s features, not the place for heavy detail, but I am curious about what happens when multiple embedded structs have methods of the same name.
I find this kind of funny:
We can access the
base
’s fields directly onco
, e.g.co.num
.
Alternatively, we can spell out the full path using the embedded type name. [e.g.
co.base.num
]
Maybe the qualified name can be used when creating an interface value when there’s ambiguity? Why do we need both?
This is an odd feature.
Generics
I have been looking forward to this. Last time I used Go this feature didn’t exist and probably wasn’t even on the drawing board.
Starting with version 1.18, Go has added support for generics, also known as type parameters.
The examples given are all fairly simple and self explanatory. A useful feature!
Errors & Custom Errors
gobyexample.com/errors
gobyexample.com/custom-errors
Errors are ugly in Go. Verbosity is the usual complaint, but worse is the idiom
of returning (T, error)
.
This goes back to sum types – or the lack of. What you want here is either T
or error
, but not both and not neither – but (T, error)
allows for both
and neither. Everyone must pretend like it doesn’t happen, and generally it
doesn’t because people are aware of the idiom, but it’s another miss for Go’s
type system. A core feature, a common and promoted idiom, which
cannot be expressed in the type system.
Goroutines
Named after the language, or was the language named for this feature? 🤔
I remember being excited about goroutines once upon a time – until I used them and realised they’re similar to threads in use. They’re not interesting without a bunch of other scaffolding, like…
Channels & …
Very cool to have as a core language feature 😎
The syntax has the whiff of a rushed prototype put into production:
Send a value into a channel using the
channel <-
syntax.
The
<-channel
syntax receives a value from the channel.
Great choice 👏👏👏 The same operator/symbol for both 👏👏👏
Could it have been a keyword or a method perhaps? Or ->
for sending and <-
for receiving?
channel <- message
message := <-channel
Not a biggie but, given that this is new and custom syntax, maybe the :=
could
have been omitted?
I lightly bound through channel buffering, onto channel synchronization where we dance around the rabbit hole of coordinating goroutines, and then onto channel directions which doubles down on the dubious syntax choice alluded to above.
Select & …
This is the good stuff now. This is what we came for. It is elegantly (nitpick:
except for the superfluous := <-
noise) built into the language.
Using select
with timeouts is both
brilliant and bothering. It’s so simple and expressive – and uses regular
structs and methods again – but I’m wondering how I can use this to have a
timeouts on each channel, not just on any channel in the select
block? I
expect we’ll find out.
Non-blocking channel operations clears up some edge cases.
Closing channels to signal that work
is done is a handy pattern, i.e. no need for a separate channel for coordinating
simple workers or pipelines, but there’s no example of using it with select
,
just with single channels. What happens if I use select
with a closed channel?
Okay, I tried it: it works the same. In both cases it’s the (T, more)
idiom.
Like with errors, this could benefit from some real sum types. Anyway, all you
haters, Go can be consistent too.
🤔 How do I use select
with an arbitrary set of channels? Hopefully we’ll find
out later.
Using range
with channels saves
some boilerplate and is readable, but I don’t care much; Go makes me type more
in other ways.
With that, day 6 draws to a close. Next time I will start with timers.