August 21st, 2024 08:16 UTC · 1 month ago

Golang

Relearning Go: Day 8

Stateful Goroutines to Recover

The bugle sounds, and we hear…

Stateful Goroutines

Instead of sharing state, use messages to communicate state. This is one of Go’s strengths, I believe, so this might have been better placed before atomics and mutexes, which we looked at yesterday.

Reading through the example – mediating access to a map that’s held by a goroutine – I get a sense of why this is a useful pattern. Were the map to be an expensive resource, something with a bit more heft, then we might also want to employ some of the earlier patterns, like rate limiting. It’s all goroutines and channels, meaning composing these together ought to be a breeze.

Sorting & Sorting by Functions

gobyexample.com/sorting
gobyexample.com/sorting-by-functions

Why is there an interface cmp.Ordered but also the builtin comparable?

The comparable interface may only be used as a type parameter constraint, not as the type of a variable.

Is this compiler magic from the Before Times, i.e. Before Generics? I guess I’m saying I don’t know how to reconcile these things into my mental model of Go quite yet.

In addition, Ordered cannot be implemented by us mortals; its powers are reserved for the Gods atop Mount Golympus. Frankly, this sucks.

Sorting is fine, but it shines a light on how Go is haphazardly put together.

Panic!

gobyexample.com/panic

Note that unlike some languages which use exceptions for handling of many errors, in Go it is idiomatic to use error-indicating return values wherever possible.

This seems to be the way many languages are going, and I’m a fan. Shame Go’s error idiom sucks though.

Defer

gobyexample.com/defer

I wrote a mini-rant about defer yesterday. Here’s a fragment:

Because defer is the way to arrange clean up, that’s the tool one reaches for instinctively – even if, for example, we’re allocating expensive resources in a loop. Imaging writing defer expensiveThing.Drop() inside a loop – because that’s how we ensure that things are cleaned up – only to find out in production that it crashes with an out of memory error. I suspect that’s an easy mistake.

Sleeping has not improved my opinion of it. Control flow is not self evident from the syntax, but, more importantly, how does it behave?

  • In what order do deferred functions run?
  • Can a deferred function write to named output parameters?
  • Can a later deferred function see those writes?
  • If a deferred function panics, are other deferred functions called?
  • What happens to values returned from a deferred function?

There are surely answers to all of these questions, but I must dig for them. I bet that most Go developers don’t know them without looking.

It sweeps the dirt under the rug.

Why bother with defer as a language construct? Instead, we could borrow the context manager approach from Python:

func with[T any, U any](build func() T, use func(T) U, finally func(T)) U {
    // …
}

In practice that’s going to be unwieldy and verbose, passing all those functions in, and then Go’s error idiom will complicate things, but it’s a bit more obvious what’s going on. To address the unwieldiness, this could be codified into a language construct instead. Let’s call it… try:

if f, err := os.Create(p); err == nil {
    try {
        // Do stuff.
    }
    finally {
        // Close the file.
        if err := os.Close(f); err != nil {
            panic(err) // Or whatever.
        }
    }
} else {
    panic(err) // Or whatever.
}

I’m not a fan of this suggestion, but at least we can see the control flow. At least we could use the knowledge and intuition we’ve developed about the language to answer my earlier questions.

Lastly, defer is often/mostly used to ensure that resources are cleaned up, but it’s easy to forget; there’s nothing forcing me to use it. Even C++ has a more elegant solution.

Recover

gobyexample.com/recover

To recover from a panic, one must defer a function that calls recover(). We get back to my questions above about defer? This is a mess.

This could have been a single function:

func recover[T any](act func() T) (T, error) {
    // … internal implementation …
}

func main() {
    if _, err := recover(mightPanic); err != nil {
        fmt.Println("Panic:", err)
    }
}

Is that not clearer?


Next time: string functions.