Relearning Go: Day 7
Timers & Tickers to Atomic Counters & Mutexes
Timers & Tickers
gobyexample.com/timers
gobyexample.com/tickers
Nothing surprising, but it’s based around channels, and I think that’s cool. We
can incorporate it into select
for example, and treat it like any other event.
The Duration
type used throughout the
time
package offers no extra type safety: it’s an alias for int64
. It’s a
count of nanoseconds. It’s also signed, meaning durations can be less than
zero moments long. Yep. Not really a duration then, is it?
This does matter. A Timer
can be created with a negative duration and it’s
okay. I assume it will expire immediately. But a Ticker
created with a
negative duration will panic. This seems like a painful inconsistency,
especially since it’s ossified into the standard library.
Duration
could have been an unsigned type, eliminating the possibility of
panic in NewTicker
and giving sense to
NewTimer
and almost every other function
in time
which accepts or returns a duration.
Worker Pools
This isn’t a specific feature, rather it’s an elegant pattern that arises naturally from a combination of goroutines and channels. It’s actually the same as any threaded worker pool, but it’s perhaps more concise, and likely to have lower overheads.
WaitGroups
A sync.WaitGroup
counts workers in and counts them out.
Note that this approach has no straightforward way to propagate errors from workers. For more advanced use cases, consider using the errgroup package.
That’s quite a limitation. Perhaps the wrong example to give? Handling errors is not Go’s forte, and this reinforces that.
This
WaitGroup
is used to wait for all the goroutines launched here to finish. Note: if aWaitGroup
is explicitly passed into functions, it should be done by pointer.
If I pass it by value, do I get a clear compilation error? Or do I get subtle and hard to diagnose bugs? That might not crop up until running in production?
Aside about defer
Starting a worker is done with a wrapper:
go func() {
defer wg.Done()
worker(i)
}()
I have come to dislike defer
. Even in this minimal example I don’t like it.
With defer
everything is done at the end of the function. 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. Imagine
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. Also, I
get that people are wary of C++, but maybe RAII was an idea worth copying?
Rate Limiting
I like how elegant burstyLimiter
is here, largely because goroutines are
lightweight enough (syntactically as well as light on CPU and memory versus a
system thread) to use as a drip feeder, putting “handle request” tokens into a
limited size channel.
Atomic Counters & Mutexes
gobyexample.com/atomic-counters
gobyexample.com/mutexes
Atomic variables: cool. Mutexes get me face palming though:
Note that mutexes must not be copied, so if this
struct
is passed around, it should be done by pointer.
Flashbacks to WaitGroup
. Anything that contains a Mutex
is now poisoned –
you must remember to pass it by pointer else Bad Things Will Happen. These are
whole classes of bugs made possible by the language design.
Also, another example of defer
being a weird language construct:
c.mu.Lock()
defer c.mu.Unlock()
c.counters[name]++
i.e. we want Unlock()
to happen after incrementing the counter, so obviously
we write it before. Note too that I didn’t include the whole function body:
you don’t and can’t know when Unlock()
is going to get called without it.
Today’s over; tomorrow’s reveille will be to the tune of stateful goroutines.