-
Notifications
You must be signed in to change notification settings - Fork 17.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: context/v2: update context package #28342
Comments
See also #27982. |
I would add to that list:
|
Related #28017 |
I have struggled with performance issues in the past with package context, which were tied somewhat to the API. This led to one hack fix and contemplation of some ugly compiler changes (#18493). I’d like any re-think of context to see how lightweight we can make the package so that it can be used in very fine-grained contexts. |
/cc @rogpeppe who I think had some ideas about the design of the API when it was first introduced. |
See also #28279. |
Perhaps have The problem with this, is that there is no way to read multiple return values, which is a pretty big issue. Not quite sure about an elegant way to solve this without having tuple types built-in to the language. Writing to the channel would unfortunately write to the channel, but if you do stupid things, you should expect stupid behavior. It doesn't have any real use except for breaking your own code. (And we can't have it return a read-only channel if we want to be able to Context of course would still exist, but it's primary purpose would be for goroutine-local values, rather than cancellation. |
I feel that |
I like this comment: #21355 (comment)
I think my only issue with it is that functions that take contexts also typically have a |
@deanveloper any example of a FooContext function that actually have 'context' in the name? I haven't seen any of that myself. But |
Partially copying my reply in #20280 As someone who has been writing Go for two months. I never really understood It is not immediate obvious for a beginner to understand the purpose It's simply not self-explanatory enough, and I don't think the terminology is consistent for a developer who also deals with concepts with the same name in a different language. e.g. writing C++ at the same time, I would think Context has server-lifetime instead of request-lifetime. I think that makes it worthwhile for us to carefully explain what it is to help engineers / new learners understand, and make the package itself as clear as possible. e.g. If it is for request-scoped info, why is it package not named [The part where I don't know what I am talking about] |
Here are a few examples off the top of my head, you can probably make a
Edit - Here's a third-party lib that I've used as well https://godoc.org/github.com/nlopes/slack |
@deanveloper oh this has to do with providing two versions of everything that either use context or not, as Go doesn't support overloading? Yeh that might need to be solved at a different level :| |
I think not supporting overloading is actually a good thing. Makes sure that code stays readable and encourages writing multiple functions for different actions (rather than using something like a boolean flag). Although it does lead to cases like this, unfortunately. |
I don't think it has anything to do with overloading, rather I think it is so because the context package was introduced fairly recently, and due to Go's backwards compatibility there has to be separate mechanisms for incrementally improving code (DialContext() as in different function, or a new context field like in net/http). If the context package was there from the start, there would probably be just one net.Dial() that takes context as first arg. |
Correct, but if Go did support function overloading, then a It was a combination of adding features in later as well as having no support for overloading. |
@deanveloper or library writers can adopt a currying approach https://golang.org/pkg/net/http/#Request.WithContext I also meant that having 'Foo' and 'FooContext' doesn't have to do with overloading, but it's just something library writers have to deal with, rather than something that would impact any decision regarding context itself here. |
I agree. I think this is due to Standard library have no use for |
|
If we drop |
The point isn't to drop WithValue from |
I don't think context package should make that easier. That's clearly a design flaw with the application, not context package.
That's exactly why context should be passed explicitly. GLS doesn't cover the same use cases that context covers. Context spans logical scopes (which consist of any number of goroutines), not goroutine or function scopes. |
That is not true for frameworks. Often the tracking logging auditing is “external” to the request processing. Implementing this without GLS is very difficult. This is the exact reason that the pprof labels are an internal implementation that a user could not implement.
… On May 31, 2020, at 9:00 AM, Antonenko Artem ***@***.***> wrote:
No way to propagate up except storing a map in the context at a higher level
I don't think context package should make that easier. That's clearly a design flaw with the application, not context package.
so using GLS for “request state” is not trivial either.
That's exactly why context should be passed explicitly. GLS doesn't cover the same use cases that context covers. Context spans logical scopes (which consist of any number of goroutines), not goroutine or function scopes.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
Problem: In large production applications (a web service in my case), it is unnecessarily difficult to diagnose Even with diligent error wrapping, the end result is often a string, like
There are many potential causes for this context cancelation. Some of them are normal operating procedure, and others require intervention. For example, it could be the request is complete, or a custom-built timeout, or perhaps a graceful application shutdown. In general, if a context wrapped with multiple Solution: I would like to propose the addition of explicit With Here's a strawman to demonstrate the concept: package example
import (
"context"
"errors"
"fmt"
)
var ErrSomethingSpecific = fmt.Errorf("something specific happened")
func Example() {
ctx, cancel := context.WithCancel(context.Background(), ErrSomethingSpecific)
cancel()
fmt.Println(errors.Is(ctx.Err(), context.Canceled))
fmt.Println(errors.Is(ctx.Err(), ErrSomethingSpecific))
// Output:
// true
// true
} |
I got frustrated with that as well and have been using https://github.com/OneOfOne/bctx/blob/master/err.go in local and work projects. |
For passing data, perhaps it would be better to use a builtin function to access the data. So instead of a global
Note that this also makes contexts much more type safe, which is a huge benefit since current contexts have the type safety of a This still doesn't feel like the best solution. But hopefully this could inspire another idea that's even better. Perhaps requiring removing context explicitly with ===== EDIT =====
|
I'd like to write a more formal proposal for what I had written above, although it seems that people don't like it. Some constructive feedback would be much appreciated. In my eyes, using builtin functions and using types as keys accomplishes three things:
The goal of what was written above is strictly for the |
@deanveloper All attempts at making context be goroutine-local state, or implicitly passed on the stack, suffer from the same issues: There are functions that need to deal with more than one context, either merging or splitting them as needed. Please read the earlier discussion using those terms. And please do write up a proposal that takes into account the whole challenge, if you think your idea is actually different from the above. And I would personally say please also make that a separate issue and link it here. And note that goroutine-local state as a concept has been firmly rejected in the past. |
Not only merging or splitting. The problem is implicit passing itself. Go is not that kind of language and people are generally opposed to that (myself included). It's much preferred that context is passed explicitly. |
Explicit passing is part of the problem this discussion exists though - this is what I mean by "context bloat" where we have every function starting with This also makes refactoring difficult. Imagine if the 5 functions above weren't initially intended to need contexts, but then after some later development, it's decided that
I beg to differ - Go likes to be explicit until things get too verbose. Specifying every single function with Think back to Java/C++ where the type of the variable is always listed with the variable itself. This got distracting, and was needlessly verbose. Both languages have since added features to remove this, and Go of course has The reason that Go is so great (in my opinion) is that it's simple. Go isn't the kind to do everything explicit - Go is the kind to do everything simple. Explicitly usually correlates with simplicity, but I am arguing that it doesn't in this case, in the same way that other features in Go such as variable type inference and garbage collection do.
My apologies, I misunderstood this. This does imply a single
I've been pretty involved in the discussion (minus the many comments discussing if the other proposed method was considered GLS or not). This proposal is mostly an addition to the one proposed earlier, although I believe it highlights the benefits and drawbacks a lot better with examples. If you are referencing this comment, I believe that 2 is a non-issue, contexts should be unidirectional. 3 is going to be hard to solve no matter what. However, this solution solves both 1 and 4 perfectly, as it is no longer untyped, and keys are namespaced by package (both of these solve 1), and because types are keys, documentation is easy to write on the provider side, and read on the consumer side (solving 4).
This is passed on the stack the same way that I'll write the proposal and link it here. Thank you both for the feedback, I will also be addressing it in the proposal when it is written. |
Well, not really. The problem is verbosity and stutter (ctx context.Context is a mouthful) and explicitness is clearly called out as desirable. That part of the issue could be fixed by making context built-in, for example.
Now imagine that contexts are implicit. Easy refactoring is impossible because context is entirely runtime concept. Compiler can't help you with it. But with explicit contexts adding or removing context argument clearly shows which call sites are affected. Implicit parameters are prone to hidden errors that can only be found at runtime either in tests or in production. That's actually one of the problems with current context implementation - it can carry arbitrary values that, essentially, become implicit function arguments. It's very hard to track down bugs around them. That's why explicitness is very important. Context clearly demonstrates function behavior. It's main usage is usually cancellation and it's very important that functions can explicitly tell that they support cancellation by accepting context argument. With implicit passing the only way to provide such intent is by documenting it for every function that expects context. Essentially, we're moving one of the function arguments into the comments. And we all know why this is bad. That brings us to why Go is not that kind of language. To save a few characters we're trading significant advantages that help with code maintainability. Go always puts that above anything else. And I don't think var/auto/:= example is relevant here. It removes verbosity but doesn't have any significant drawbacks because these languages are statically typed. I would still like to see a more concrete proposal but I just think in its current state it would be heavily downvoted. Any kind of implicitness is a tough sell for Go. There's been too many examples of that. |
This is not how a function says that they support cancellation. A function should state that it supports cancellation by documenting that it supports cancellation. The only thing that accepting a context states is that it may take some arbitrary data as input. For instance in the
Also, perhaps my function doesn't support cancellation on its own, but its callees do. Now, my callees can return a cancellation error which propagates up the stack without the middle-men needing to support cancellation themselves.
This point is somewhat fair. But it depends on what you mean by "easy refactoring", which isn't strictly defined. Easy refactoring could be talking about only needing to change 2 lines in the 2 places that you're concerned about, rather than 9 lines potentially scattered around several files. (9 lines: 2 lines for every function (the arguments and the next function call), minus 1 line because a() doesn't need to change it's signature). However, easy refactoring could also mean type-safe-refactoring, which allows us to
This isn't about saving a few characters, this is about reducing the amount of bloat on many functions that are written.
It's actually very relevant, it's almost the exact same issue. Languages like Java/C++ took so long to implement var/auto. This isn't because they couldn't (at least in later years), they didn't implement var/auto because they didn't want to. They thought that keeping the type along with the variable was verbose, but that verbosity made things significantly clearer. However after languages like Swift, Go, Kotlin, Rust, and several others, they saw that removing the verbosity of explicitly defining every variable's type actually made code easier to read. My argument is that passing a context into nearly every function is the same way. I much appreciate that Go is a verbose language. But in some applications, it is extremely useful for contexts to be passed implicitly rather than explicitly. |
As I was writing a formal proposal for this, it just kept getting more and more cumbersome. Adding more builtin functions, describing how the contexts get passed, it just gets worse and worse. Ideally the best change (imo) would be to bake cancelling into the language, keep "contextual data" the way it is, and adding some kind of |
How about stack-local storage with explicit 'context parameters' Parameters are matched by type (to avoid parameter name/position clashes) so there is only one possible context.Context 'context-parameter'. Can easily be overiden, trivial to deal with multiple contexts (as you can still pass them to a function as a normal parameter). Still explicit when it matters. Works with methods and generics. Prevents context poisoning and is flexible to different context types. //DoSomething has a context parameter.
//any functions that 'DoSomething' calls will transparently & recursively
//pass ctx along to any functions with an explicit context paramter.
func{ctx context.Context} DoSomethingWithContext() {
DoSomethingWithoutContext() //automatically passes ctx to this function.
DoAnotherThingWithContext() //automatically passes ctx to this function.
DoAnotherThingWithContext{context.Background()}() //override the passed context with a different context.
}
func{ctx context.Context} DoAnotherThingWithContext() {}
func DoSomethingWithoutContext() {
DoAnotherThingWithContext{context.TODO()}() //override the passed context with a different context.
DoAnotherThingWithContext() //passes the implicit context provided by the caller.
} context.Value is no longer needed as packages can declare their own private 'context types' and pass them as type-safe 'context paramaters' across API boundaries. type myValue string
func{val myValue} CheckMyValue() {
fmt.Println(val)
}
//DoSomething could be defined in a different package.
func DoSomething(do func()) {
do()
}
//Prints Hello World
func main() {
DoSomething{myValue("Hello World")}(CheckMyValue)
} Compiler can optimise away the passing of the 'context parameter' when it knows no functions that can handle the parameter are ever called. |
I feel like I would like this better if it didn’t mean that Go could have 3 potential parameter lists for a function (after type parameters are added) edit - also, what happens if the context hadn’t been provided? |
I guess I personally don’t see the benefit of adding a language feature for contexts when they aren’t widely used outside of cancellation. A much better idea (in my eyes) would be to bake cancellation into the language, and improve |
It is read as an empty value (nil).
What does this mean? What does this look like? |
How would I differentiate wanting to pass an empty value vs the value not being provided at all? Presumably if I have a
This means to have cancellation as a native feature in Go. I’m not sure what it would look like, I (and hopefully others) should think about that. |
Cancellation as a native feature in Go could be implemented in many ways. The simplest form could be like the current A different idea would be to allow go routine management like the OS allows signalling between processes, which would mean that we would have some way to send a termination signal to a go routine from another go routine. Such a signal implementation would be very different from I'm sure there are even more options on how to implement go routine cancellation natively in Go. The question for me is really if it is worth to implement go routine cancellation as a standalone feature in Go and if that cancellation support would be as versatile and relatively easy to understand as what we currently have with |
The first and second are relatively easy to do manually with a |
https://golang.org/ref/spec#Function_types Consider a breaking language change, a shorthand for package main
import (
"context"
"fmt"
)
type ctx = context.Context
func getname(ctx ctx) (string, bool) {
u, ok := ctx.Value("namekey").(string)
return u, ok
}
func main() {
ctx := context.WithValue(context.Background(), "namekey", "goku")
u, ok := getname(ctx)
if !ok {
panic("no user")
}
fmt.Println("user is", u)
} what if we had func getname(ctx) (string, bool) {
u, ok := ctx.Value("namekey").(string)
return u, ok
} at our disposal, all else being equal to the first code block?
vs
|
For the stuttering, around the time the context library was created, I created a library with similar functionality with a context type called execution.Context If we make it so that every function on a single goroutine share an implicit execution context, the problem is going to be about how to propagate changes to this execution context down the call chain. For instance if we want to modify the interface by wrapping it. I suspect that explicitness as is done nowadays, might be the most practical way. |
I'm currently working on a proposal:
Would people be interested in this? My main concern is for the |
@deanveloper Sounds interesting. Would be nice to see your proposal, and how it compares to ctx.Done(), etc. |
If it was just some it would be good. Unfortunetly the current Context solution has become infective, adding unneccesary noise to almost all method and function signatures. If we split the two uses, that of Cancellation and that of "store", Suppose we have way to name Scopes and pas a "context.ContextLite"(offering only the cancellation mechanism) WithContext(ctx: ContextLite){ This sounds very similar to TLS/GLS but it would be limited to just the context type for cancellation. Ofcourse there is no reason why to limit this to just cancellation and not be a real TLS/GLS but the problems are distinct,. |
This issue is intended to cover ideas about updating the context package for Go 2.
ctx context.Context
.Context.WithValue
function accepts values of any types, which is easy to misuse by passing, say, a string rather than a value of some package-local type.Context
is confusing to some people, since the main use of contexts is cancelation of goroutines.See also #24050 and #20280.
The text was updated successfully, but these errors were encountered: