I should assume contexts aren't retained in Go APIs

January 28, 2023

Over on the Fediverse, I said something about some Go APIs I'd run into:

Dear everyone making Go APIs that take a context argument and return something you use to make further API calls (as methods): please, I beg you, document the scope of the context. Does it apply to only the initial setup API call? Does it apply to all further API calls through the object your setup function returned? I don't want to have to read your code to find this out. (Or to have it change.)

I was kind of wrong here. While I still feel that your documentation might want to say something about this, I've come around to realizing that I should assume the default is that contexts are not retained. In fact this is what the context package and the Go blog's article on contexts and structs say you should do.

I'll start with the potential confusion as a consumer of an API. Suppose that the API looks like this:

handle := pkg.ConnectWithContext(ctx, ...)
r, e := handle.Operation(...)
r, e := handle.AQuery(...)

Your (my) question is how much does the context passed to ConnectWithContext() cover. It could cover only the work to set up the initial connection, or it could cover everything done with the handle in the future. The first allows fine-grained control, while the second allows people to easily configure a large scale timeout or cancellation. What the Go documentation and blog post tell you to do is the first option. If people using the API want a global timeout on all of their API operations, they should set up a context for this and pass it to each operation done through the handle, and thus every handle method should take a context argument.

Because you can build the 'one context to cover everything' usage out of the 'operations don't retain contexts' API but not the other way around, the latter is more flexible (as well as being what Go recommends). So this should be my default assumption when I run into an API that uses contexts, especially if every operation on the handle also takes a context.

As far as documentation goes, maybe it would be nice if the documentation mentioned this in passing (even with 'contexts are used in the standard Go way so they only cover each operation' as part of the general package documentation), or maybe this is now just what people working regularly in Go (and reading Go APIs) just assume. For what it's worth, the net's package DialContext() does mention that the context isn't retained, but then it was added very early in the life of contexts, before they were as well established and as well known as they are now.

I feel the ice is thinner for documentation if the methods on the handle don't take a context (and aren't guaranteed to be instant and always succeed). Then people might at least wonder if the handle retains the original context used to establish it, because otherwise you have no way to cancel or time out those operations. But I suspect such APIs are uncommon unless the API predates contexts and is strongly limited by backward compatibility.

(These days Go modules allow you to readily escape that backward compatibility if you want to; you can just bump your major version to v2 and add context arguments to all the handle methods.)

(Now that I've written this down for myself, hopefully I'll remember it in the future when I'm reading Go APIs.)

Written on 28 January 2023.
« Some thoughts on whether and when TRIM'ing ZFS pools is useful
The CPU architectural question of what is a (reserved) NOP »

Page tools: View Source, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Sat Jan 28 22:27:31 2023
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.