The Joy of Reading Source Code
This morning I was drinking my coffee and eating a pastry while reading about how you shouldn’t defer Close() on writable files and dove into the HN comments. The first comment calls out a minor improvement that can be made to the code. Specifically, using errors.Join
to combine the returned errors in a succinct and clear way. I wasn’t familiar with errors.Join
so I decided to dive into the source code a bit.
A bit of an aside, but I use Dash mapped to opt+<space>
to open documentation quickly, and found errors.Join
. This took me right to the Go documentation, which has this excellent feature where clicking the method name takes you right to the source code. This made it really easy to dive into the documentation and source of errors.Join
at a whim.
What I learned from errors.Join
The errors.Join
source is pretty simple, but it has a few interesting things going on. Overall the code is short and direct, but the joinError
struct and associated methods had me asking a few questions.
The first question was almost immediately “how does this work with Unwrap
?” and the answer is that it doesn’t. The documentation for Unwrap
actually calls this out too, since the signature is Unwrap() []error
and not Unwrap() error
. This makes sense to me, since handling slices of error
would make Unwrap
significantly more complex and have some less-than-desirable performance implications. Despite the API gap, I think it’s the correct design decision since there’s likely no need to optimize for edge cases like this one.
It would also be simple to handle a top-level Join
unwrap if needed too. e.g.
type joinUnwrapper interface {
Unwrap() []error
}
// AnyIs supports checking if any error in the provided err contains
// the target error. It differs from `errors.Is` by supporting `errors.Join` errors.
func AnyIs(err error, target error) bool {
if unwrappable, ok := err.(joinWrapper); ok {
for _, e := range unwrappable.Unwrap() {
if errors.Is(e, target) {
return true
}
}
}
return errors.Is(err, target)
}
After writing this code, I think it might be a bit of a smell. If I controlled the code that emits the errors.Join
error I’d likely update it to better encapsulate certain error conditions to avoid the need to loop over errors in this way.
The second question I asked was more around API ergonomics. How does errors.Join
represent the Error() string
value? It’s formatted how I’d expect with each error having its own new line, but it is doing something interesting under the hood. Instead of using a strings.Builder
or casting bytes to a string like I’d have (potentially naively) expected, it’s using unsafe.String
. It’s a micro-optimization to avoid extra allocations you’d get from strings.Builder
or string(b)
that I haven’t had to reach for yet, but now I know about it and have another (sharp) tool in my toolbox.
That was effectively where the source code ended and I had finished my pastry, so I decided to get back to work after this brief respite.
It’s a lot of fun diving into the source code of projects you use day-to-day and this was no different. In this case even though it was a quick read of a well scoped API I learned a thing or two. I got to see how the authors approached implementing the API under-the-hood which was different than how I imagined it, and I also got to learn about unsafe.String
and how it’s used in production.