Skip to content

proposal: context: new package for standard library #14660

Closed
@bradfitz

Description

@bradfitz

@Sajmani and I are interested in adding golang.org/x/net/context to the standard library, probably as the top-level package context.

(Why not net/context? It has little to do with networking, except that people often use it with networking; it doesn't even cross network boundaries on its own without help.)

Some places in the standard library which could use it:

  • https://golang.org/pkg/net/http/#Request -- add a new Context field, alongside like the existing (Context.Done-compatible) Request.Cancel field. It will take precedence over Cancel, and we can make it work for both outbound and inbound requests. For server requests it would use CloseNotifier's mechanism for c* ancelation. (which for http2 includes the http2-level explicit stream cancelation frame too, also used by gRPC)
  • https://golang.org/pkg/net/#Dialer -- same as http.Request. net.Dialer also has a Done-compatible Cancel field, which could be a Context.
  • https://golang.org/pkg/os/exec/#Cmd could have a new Context field to automatically kill+wait the process when a context expires, if the context expires before the child?
  • https://golang.org/pkg/database/sql/#DB.Query already takes ...interface{} which can be special-cased to say that if the first parameter is a context.Context, it's used for deadlines. Trashy or clever? Maybe both. We could also add parallel DB.QueryContext which is a bit of a mouthful.
  • .... (where else?) ....

Open questions:

  • do we do something with the io package in the same release, or later? i.e. do we add *os.File.IO(context.Context) io.ReadWriteCloser sort of methods, to curry a context into io interfaces? Or wait on that. If it'd be nice to push down cancelation into even OS-level file I/O, but we don't even really do that inside Google with IO-like methods and pervasive Context usage, since we don't really use *os.File. So I'm inclined to wait on that. Too many operating systems to deal with.

Concerns:

  • we can't add it to as many places in the standard library we'd like to since APIs are frozen. Basically we can only add it to structs. While we've told people not to add contexts to structs, I think that guidance is over-aggressive. The real advice is not to store contexts. They should be passed along like parameters. But if the struct is essentially just a parameter, it's okay. I think this concern can be addressed with package-level documentation and examples.
  • what field name in structs to use? We use "ctx" for variables, but struct names are typically spelled out. But does that cause too much stutter? e.g.
package http
type Request struct {
    Context context.Context
}

Activity

self-assigned this
on Mar 5, 2016
elithrar

elithrar commented on Mar 5, 2016

@elithrar

For others following, there was a substantial discussion on golang-dev about this as it related to net/http: https://groups.google.com/forum/#!topic/golang-dev/cQs1z9LrJDU

As for naming: the only stutter is in the field name in the source. Package users are going to call r.Context.WithValue or r.Context.Done, which has the benefit of being explicit and (say you point out, Brad) retains the ctx naming convention for variables.

Looking forward to this happening!

atdiar

atdiar commented on Mar 5, 2016

@atdiar

Thinking about http.Handler, people can alternatively rewrite a different kind of handler that has a different signature for ServeHttp. The advantage is that one can easily add a Context parameter.
But also, add return values. For instance a boolean sentinel value to control whether the handler should be the last one to be executed. Or a different http.ResponseWriter (useful if you wrap the ResponseWriter in one of your handlers, for instance when you gzip things, but not only)

The top level mux object that encapsulates the whole request dispatching logic can still remain a regular http.Handler (and the context.Background would be instantiated in its ServeHttp(w,r) and passed to the handler ServeWithCtx(ctx,w,r)). Probably better that I upload an example of what I mean.

There are also a few things that I am not very fond of. context.TODO is one of them. But I don't know if it is too late to change things or not.
Is this the right place to discuss about API design ?

jimmyfrasche

jimmyfrasche commented on Mar 5, 2016

@jimmyfrasche
Member

I think naming it Context in structs is good. There's a stutter, but only for the author of the API. Couldn't be clearer for the users of the API.

Would it be possible to write a go vet check for obvious misuse of a Context in a struct? If so, it would be great to roll that out at the same time. Posting a sign next to the pool is good, but a lifeguard is better.

As much as I'd love Context to be deeply baked into the standard library, I'd be happy with it simply being added to the standard library to say "this is blessed, find good ways to use it" and later adding the best ideas to the stdlib where they can be and kept in third party packages where they cannot. However, I'm not opposed to any of the suggested integrations.

Having the first interface{} parameter to Query be a Context is clever, though, so I'd much rather have the explicit QueryContext.

riannucci

riannucci commented on Mar 5, 2016

@riannucci

This is maybe a bit out of place for this particular issue, but I think it would be worth considering a different Context implementation (strawman benchmark) before it gets included in the stdlib.

In that benchmark, I compare a bogus map+lock "caching" context implementation to the default implementation. Even with as few as 10 items, there's a noticeable improvement (%-wise, not in absolute terms. I realize we're talking nanoseconds here) on lookups into the cached version. For contexts deeper than 10 items, there's a noticeable improvement. The construction overhead is similar, but I think with a non-bogus implementation the map version could be even faster for overhead than the linked list one.

I think there's an opportunity to have Context be a *struct-with-hidden-fields instead of an interface, which would afford the opportunity to build a faster implementation of it (e.g. by having .WithValue be a struct method instead of a package method that can be smart about chaining, or a .WithValues to avoid the linked-list-building overhead when adding multiple items to the context at the same time) and by possibly adding a .Compact method to compactify an existing Context to make subsequent lookups fast. It's not possible to build these things in a clean fashion with the current interface-based implementation (since the only way for the context to see up the chain is to actually do lookups, and the only way to know the lookup keys is to wait for some user code to provide them).

I'm mostly thinking about the pattern of:

func MyHTTPHandler(rw ResponseWriter, r *Request) {
  c := context.Background()
  // WithValue 10-30 services/contextful things here... this number will likely grow
  // as context becomes even more prevalently used.

  // 100's of lookups into c here... if it were compacted here then all the lookups
  // would be O(1).
  myActualHandler(c, rw, r) 
}

Which, I think, is a pretty common pattern (I don't have data on hand for this assumption, however).

In general, for most stdlib-type libraries, there would simply be competition for the best implementation. But I think that the community is rallying around a single Context implementation (because it's THE way to build composable/stackable libraries which are loosely coupled). There can't really be an alternate implementation that doesn't stem from the stdlib (or x/) that has any hope for survival (since everything needs to adopt the stdlib one to actually be used by anything). To be clear: I think it's GOOD for the community to standardize here, I just think it might be worth considering a slightly speedier implementation before cooking it into the 1.x API compatibility guarantee.

Note that the WithTimeout and WithDeadline package functions are well-served by the interface implementation right now: it allows e.g. mocking of the clock. Some similarly mockable interface would need to be built for that functionality, but AFAIK that mocking requirement doesn't really apply to WithValue.

edit: typo

kr

kr commented on Mar 5, 2016

@kr
Contributor

How do you expect putting context in http.Request (or any other parameter-ish struct) will interact with adding values (or timeouts or whatever) to the context?

I can think of two basic approaches, caller-save and callee-save:

func f(w http.ResponseWriter, req *http.Request) {
    ctx := req.Context
    g(w, req)
    // ... something with ctx ...
}
func g(w http.ResponseWriter, req *http.Request) {
    req.Context = context.WithValue(req.Context, foo, bar)
    h(w, req)
}

or

func f(w http.ResponseWriter, req *http.Request) {
    g(w, req)
    // ... something with req.Context ...
}
func g(w http.ResponseWriter, req *http.Request) {
    req2 := new(http.Request)
    *req2 = *req
    req2.Context = context.WithValue(req.Context, foo, bar)
    h(w, req2)
}

Of course, it's fine if both caller and callee save the old value, but it's bad if neither of them does it, each expecting the other to do so. So, should we recommend one and document and promote it up front?

rakyll

rakyll commented on Mar 5, 2016

@rakyll
Contributor

Traditionally, context objects should carry configuration. The x/net/context package extends context objects' responsibilities by providing constructs for timeout and cancellation signals. In the scope of stamp coupling of request handlers, it is a valuable addition. But I am not sure how it will scale to the rest of the standard library and furthermore whether the context may end up being abused for signaling purposes. The two cases you mention above (exec.Cmd and sql.DB.Query) does this by solely using a context object for handling signals even though there is no necessity for storage. Are you trying to solve the issue of how data is passed among functions or also trying to have primitives around how the entire execution could be signaled from outside?

One of the concerns I would add to the list is that the context should be scoped to a function call.

"Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx" (link)

If the standard library won't be agreeing with this due to compatibility reasons, don't you think it will create confusion?

bradfitz

bradfitz commented on Mar 6, 2016

@bradfitz
ContributorAuthor

@rakyll, I addressed that final concern in my first post. ("While we've told people...")

added this to the Go1.7Early milestone on Mar 6, 2016
zippoxer

zippoxer commented on Mar 7, 2016

@zippoxer

If there's any way to add context to the standard library without making http or net aware of context (can still adjust them to play well with it though), this would be the way to not overwhelm newcomers reading the docs.

okdave

okdave commented on Mar 7, 2016

@okdave
Contributor

The plan @bradfitz is proposing makes http and net aware of context, but not in a way that requires their use for basic cases. The http package is already very large, but you only need to use a very small fraction of it to achieve "hello world"-esque examples. Adding context to these packages won't harm newcomers, and should make life easier in more complicated/large codebases.

bradfitz

bradfitz commented on Mar 8, 2016

@bradfitz
ContributorAuthor

I mailed out https://golang.org/cl/20346 &https://golang.org/cl/20347

The first copies x/net/context to the standard library. The second reworks x/net/context to just use the standard library's version.

59 remaining items

minux

minux commented on May 5, 2016

@minux
Member
bradfitz

bradfitz commented on May 5, 2016

@bradfitz
ContributorAuthor

@minux, no rush. Eventually. Maybe at Go 1.8 time.

phuslu

phuslu commented on May 8, 2016

@phuslu

@riannucci

I share you concern about the WithValue linked-list performance.

Now I use below pattern in my codes,

func MyHTTPHandler(rw ResponseWriter, r *Request) {
  c := context.Background()
  c = c.WithValue(c, "placeholder", &struct{Member1 string, Member2 string, Member3 int}{"foo", "bar", 42})
  myActualHandler(c, rw, r) 
}

Do you think it could help the looking up performance or not?

Thomasdezeeuw

Thomasdezeeuw commented on May 9, 2016

@Thomasdezeeuw
Contributor

@phuslu instead of guessing, why not write a benchmark?

phuslu

phuslu commented on May 9, 2016

@phuslu

@Thomasdezeeuw Thank you.

But the more important thing(I think) is, Does this pattern is the RIGHT practice of context.Context ?

As you see, the original context.Context is used as a nested structure and seems that each node is immutable(Please correct me if I misunderstand). the original common pattern is,

ctx = context.WithValue(ctx, "placeholder", ...)

And the pattern which I used is treat context.Context as a mutable object -- although it may has potential performance improvement.
It will encourage people use below code against context.Context

ctx.Value("placeholder").(*struct {}).Member1 = ...

It smells bad. Hopefully could get comments from you and others.

bradfitz

bradfitz commented on May 9, 2016

@bradfitz
ContributorAuthor

@phuslu @riannucci @Thomasdezeeuw, please move discussion of Context.WithValue performance to a new bug.

sonatard

sonatard commented on Jul 8, 2016

@sonatard

Hello!!
I have a question about sharing values between middlewares.

I think we'll be able to share values between middlewares in the following code by go 1.7.

func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        user := GetUserFromRequestCookie(r)
        ctx := r.Context()
        ctx = context.WithValue(ctx, "user", user)

        r = r.WithContext(ctx)

        a.next.ServeHTTP(w, r) // next handler is ArticleHandle
}

func (a *ArticleHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() 
    user := ctx.Value("user").(*User)
}

But r.WithContext(ctx) exec shallow copy for goroutine. I need not shallow copy for only sharing values. It is slow. right? I want to set context to Request. But now it cannot.

func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        user := UserFromSession(r)
        ctx := r.Context()
        ctx = context.WithValue(ctx, "user", user)

        // r = r.WithContext(ctx)
        r.SetContext(ctx)

        a.next.ServeHTTP(w, r)
}

func (a *ArticleHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() 
    user := ctx.Value("user").(*User)
}

Please tell me best way sharing values between middlewares using only net/http and context. I think we should not implement context wrapper like 3rd party framework impl for only sharing values.

davecheney

davecheney commented on Jul 8, 2016

@davecheney
Contributor

@sona-tar We don't the issue tracker to ask questions. Please see https://golang.org/wiki/Questions for good places to ask. Thanks.

locked and limited conversation to collaborators on Jul 14, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @bradfitz@kr@davecheney@elithrar@pkieltyka

        Issue actions

          proposal: context: new package for standard library · Issue #14660 · golang/go