August 19th, 2024 08:14 UTC · 4 months ago

Golang

Relearning Go: Day 6

Enums to Select

Enums

gobyexample.com/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 embeds base, the methods of base also become methods of a container.

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 on co, 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

gobyexample.com/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

gobyexample.com/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 & …

gobyexample.com/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 & …

gobyexample.com/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.