Context should go away for Go 2
faiface.github.io
faiface.github.io
A solution? Well, I'd had a few passing thoughts but never really vetted them much or thought of implementation, just throwing things at the wall. I'll share anyways.
A) A supervisor type system. the 'go' command would return some id, eg
x := go runthis()
where x is the id. This could then be used to identify, kill, etc things at a supervisor level.B) Maybe more esoteric, but treating go routines as implicit channels. There's a few ways this could go, but it would be similar to the above, except the return would be an channel. This could allow passing signals to the goroutine, and also, retrieving values from the goroutine. Eg
x := go run()
y := <-x // y is the return value of x
or
x := go run()
x <- 1 // terminates the x goroutineI like B, but a major use case for Go is http servers, and I can see that being awkward when most frameworks don't expose the creation of the gorountine.
I find this a weird stance. Idiomatic Go already has pervasive mutable globals, and life before main. Aside from cancelation the other thing that `context.Context` poorly abstracts is goroutine local storage. Having go routine ids and the new sync.Map seem like a backwards compatible way to add these features to go now.
ChildChannel.dependsOn(ParentChannel)
for explicitly deciding when you want to end a whole tree of computation.
Way back when, Martini solved this problem using reflection (which was met with vitriol). Other packages solved this with their own handrolled context object (which was always a map[string]interface{}). I personally always liked how Martini handled it (you got to "keep" your types), but understandably it included way too much magic.
It's not the verbosity that bothers me. It's the arbitrary "verbosity is bad/verbosity is good" choices that have emerged over time.
Roughly, languages like Java add length by long identifiers and numerous modifiers to convey that something belongs to particular semantic category that behaves in a particular way.
Go adds length by having fewers such categories and forcing you to explicitly code up the behaviour. Short identifiers (and proper use of scoping!) are orthognal to this, except insofar as they reduce the pain.
That's nice, but it doesn't sovle the fact that the auther (rightly!) doesn't want to add extra complication to interfaces like io.Reader
Read([]byte)
Read(context.Context, []byte)
In a way reflection let you do operator overloading, so in your case you could just call a.Read(p) and others could a.Read(ctx, p). In Martini's case, if you wanted your handler to be cognizant of a cancellable context, you would just add the context parameter.- send signals by exposing a channel
- serves as a bag of values (the infamous .Values method ).
Context was invented because Go developers kept writing leaky servers as they were referencing *http.Request outside an http.Handler (for session management for instance) . If you do that, you need to be able to signal that the request treatment is done or you will have a memory leak.
I guess they added .Values(interface{})interface{} because they could.
Cancelation is just one aspect of a generic concern: that a tree of function calls (and more importantly, RPCs) is a causal chain of events that external systems (including humans) will want to monitor and control. Trace IDs, auth tokens and other things need to be propagated somehow, but the code that they propagate through shouldn't care about them.
So its nice to have context.Context in the standard library so that we don't have a proliferation of per-library hand-rolled solutions which then have to be virally propagated.
A (not very good) example in pseudocode would be:
complicated_operation(param) with context (log_out=myfile);
// complicated_operation calls a function,
// that calls another function...
void log(string str) context (File log_out=stdout) {...};
This would be a bit like TLS, but has the advantage that the context reverts after the original call.Usually, when I have this problem, I just stuff all the functions into a class, and make the "context" private fields. But then you end up with classes that really "don't want to" be classes, the lifetime of the context state is longer than necessary and unclear, and it feels like a hack. Also, in many languages going from free functions (and plain data and closures) to classes takes a bit of rewriting due to different syntax.
The only language I'm aware of that has these "dynamic scopes" is (apparently) Emacs Lisp, but it should be possible to fake them in C++ quite well.
It would also make the situation in the article a lot cleaner, IMO.
using(TransactionScope scope = new TransactionScope()){
//do stuff
}
Any connection created while that block is active will 'enlist' in the transaction associated with that scope. This works (I think...can't find it now) by using something called 'CallContext', which is more or less a static key-value store that's persisted up and down the call stack (including through async continuations, not sure how that works).If the 'CallContext' is static, doesn't that mean it's global and shared between threads? Well...in C# that's not necessarily true since the thread is accessible via a static property and it's possible to store data against the thread (which is exactly what 'CallContext' does, plus some magic that moves the data during async continuations in case the execution thread changes)
(call-with-dynamic-bind ((var1 val1)(var2 val2)...) stuff...)
It lets you introduce dynamically scoped variables that work pretty much the way you described.
(There is a dynamic binding operator called progv, but the situation for that is when the variable names are computed.)
So dynamically scoped contexts are achieved just by binding these special variables. By convention, they are usually given names beginning and ending with an asterisk ("earmuffs") to put them into an effectively separate namespace.
That parametrize is reminiscent of fluid-let; another Scheme hack to simulate dynamic scope.
Most of the major scheme implementations I've used have had real dynamic scoping of some sort. I don't think it's very well supported in the standard, but they all seem to have it as a proper language feature.
(with-output-to-string (*standard-output*)
... code ... )
Here, code's output to the default standard output stream is captured in a string.> but it should be possible to fake them in C++ quite well.
Been there done that; had a dynamic_var<T> template some seventeen year ago. It provided thread-specific rebinding and all.
https://golang.org/doc/faq#What_is_the_purpose_of_the_projec...: "By its design, Go proposes an approach for the construction of system software on multicore machines."
That page points to https://talks.golang.org/2012/splash.article for "A much more expansive answer to this question". That article states:
"Go is a programming language designed by Google to help solve Google's problems [...] More than most general-purpose programming languages, Go was designed to address a set of software engineering issues that we had been exposed to in the construction of large server software."
I think a solution would probably relate to looking at client connections.
Then again, I'm positive I'm not as smart as numerous google (and golang) engineers that have already thrown around ideas so maybe this isn't as big a deal as it seems.
Not sure I have a solution, but I'd love to see someone come up with one before context "infects" every function related to I/O.
This made me twitch because ultimately no argument should start with, "I use it this way and not that way, so change what you're doing." This was followed by:
> For this reason, when designing the Go language and it’s standard library, we need to approach it from a general purpose language perspective.
My understanding of what's being said here is simply: "I code in Go in this particular way, so it should be made to suit those requirements." Just because you don't write servers in Go it doesn't mean others don't. I personally enjoy the production ready HTTPS server and client on a daily basis. I'm constantly writing APIs in Go.
All I'm saying here is don't start by saying something doesn't suit your needs so it needs to change.
The rest of the article was great and gave me more insights into how Go works, so I thank you for that. And it's possible there is a better solution for context that should be considered for v2.
Thanks for the good read.
So far, I think Go does a good job of using context.Context where it is needed and not letting it leak into things like io.Reader. But it is a tricky balance to strike, because the the author is right that the context pattern is viral by nature.
It's general in nature in that you can ignore certain features and use it as a general language, but ultimately it has a built in HTTPS web server for a reason, right? And a programmer and an SRE, Context is an excellent idea for tracing requests through the whole stack -- again, there was a purpose to this design.
I'm in the odd position that when I go to work as an SRE at Google, I hardly use Go, but at home I like to use it for hobby projects which are rarely servers. When I do use Go at work, it is for tools that I run on my workstation.
So (a) even at Google there is more to life than servers, and (b) Go benefits from being attractive to people who are not at Google and/or not writing servers.
Ansible has one goal in mind: configuration management. It can also talk to my toaster and send a Tweet when I pop some bread in there. I guess Ansible is a "general purpose" configuration management tool, except it's not :-)
Terraform can be used to run a PowerShell script on a Windows box it never provisioned. I guess it's a scripting framework now.
I think Go can be used as a general purpose language, but that's not how I see its design ethos. It caters to that crowd, but does have an underlaying goal I believe.
As long as people are happy and things are working for them then all is well, I guess.
https://golang.org/doc/faq#What_is_the_purpose_of_the_projec...:
"By its design, Go proposes an approach for the construction of system software on multicore machines."
They also refer to https://talks.golang.org/2012/splash.article for "A much more expansive answer to this question". That article states:
"Go is a programming language designed by Google to help solve Google's problems [...] More than most general-purpose programming languages, Go was designed to address a set of software engineering issues that we had been exposed to in the construction of large server software. "
There's a few more warts that have been added since Go 1.0, presumably to preserve backwards compatibility. Magic comments, the forking of the syscalls package, the vendoring experiment, the alias functionality, etc.
To me it sounds like having to pass a context everywhere is something that should be handled on the language level, just as concurrency.
My first reaction was that monads would solve the problem. Since this is go hardcoding cancellation intonthe language probably is the best available solution, though.