The Joy of Reading Source Code
This morning started my morning 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.
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.