[HN Gopher] Context should go away for Go 2 (2017)
       ___________________________________________________________________
        
       Context should go away for Go 2 (2017)
        
       Author : hiohio
       Score  : 104 points
       Date   : 2025-01-21 08:08 UTC (14 hours ago)
        
 (HTM) web link (faiface.github.io)
 (TXT) w3m dump (faiface.github.io)
        
       | the_duke wrote:
       | Needs a (2017)!
        
         | pansa2 wrote:
         | Yes, I was about to comment that "there won't be a Go 2", but I
         | guess that wasn't settled when the article was written.
        
           | riffraff wrote:
           | as someone who's not in the community: why not?
        
             | orian wrote:
             | To not repeat other's (Python) mistakes ;-)
        
               | phire wrote:
               | The the introduction of Python 3 wasn't a mistake. The
               | mistake was discontinuing Python 2.
               | 
               | Just look at how rust does it. Rust 1.0 code still works
               | in the latest version of rustc, you just need to set the
               | project to the Rust 2015 edition. You can even mix-and-
               | match editions, as each crate can have a different
               | edition. Newer versions of rustc will always support all
               | previous editions, and breaking changes are only ever
               | introduced when a new edition is released every 3 years.
               | If the crate is stable, no real reason to upgrade, it
               | will work forever. And if you do need to update a
               | project, you can split it into multiple crates and do it
               | incrementally.
               | 
               | Just imagine how much smoother the python 3 transition
               | would have been if you could transition projects
               | incrementally, module by module as needed.
        
               | 9rx wrote:
               | It seems you are both saying the same thing. Had Python
               | not introduced a line in the sand and instead continued
               | to support Python 2 amid future updates there would have
               | been no reason for Python 3. The Python 2 line could have
               | kept improving instead.
               | 
               | Just as you say, Python could have introduced what is
               | found in Python 3 without breaking Python 2 support.
               | Which is the direction Go has settled on; hence why Go 2
               | is off the table. Go 1.0 and Go 1.23 are very different
               | languages, but backwards version support is retained, so
               | no need for a new major version.
        
               | phire wrote:
               | No. The point of rust editions is that they do break
               | support for older code, which is very different to what
               | go has now settled on.
               | 
               | IMO, it's the best of both worlds. Old code continues to
               | work forever, but your language design isn't held back by
               | older design mistakes.
        
               | 9rx wrote:
               | The trouble with the Rust community is that it is
               | terrible at communication. That may be why you extend a
               | presupposition that everyone understands the meaningful
               | difference between Rust editions and Go version
               | directives, but I can't tell a difference beyond the
               | frivolous like syntax used. Based on the documentation of
               | each they seem like the exact same concept, with the
               | exact same goals in mind. As a result, unfortunately,
               | your point is not yet made. Perhaps you can break the
               | cycle and describe for every day people how Rust editions
               | are fundamentally different?
        
               | tubthumper8 wrote:
               | The documentation is probably the best resource to start
               | with the concept and how it works / what the goals are:
               | https://doc.rust-lang.org/edition-
               | guide/editions/index.html
               | 
               | For example, https://doc.rust-lang.org/edition-
               | guide/rust-2021/warnings-p... - code that produced a lint
               | warning in the 2018 edition produces a compiler error in
               | the 2021 edition. That would be something that can't be
               | done in a backwards compatible way without editions
               | 
               | Another example would be changes to the import syntax
               | https://doc.rust-lang.org/edition-guide/rust-2018/path-
               | chang... - the compiler will forever support the 2015
               | behavior in crates that use the 2015 edition, but crates
               | using newer editions can use the newer behavior
        
               | 9rx wrote:
               | As stated before, the documentation in both languages was
               | already consulted. It did not clear up how Rust is any
               | different than Go in this regard. Consider a simple
               | example from the Go documentation: Support for numeric
               | underscores, which was not a part of the original
               | language and later included in a 'new edition' of Go.
               | i := 10_000_000
               | 
               | Using the 1.13 or later version of the gc compiler, if
               | your go.mod specifies anything after 1.12 the above
               | compiles fine. But if go.mod asserts go 1.12 or earlier,
               | you will get a compiler error from the above code as the
               | compiler reverts to 1.12 or earlier behaviour based on
               | the version directive. That sounds exactly like what you
               | described! And, like I said before, Rust's documentation
               | too echoes to my read that editions accomplish basically
               | the same thing and exist for the same reason Go version
               | directives exist.
               | 
               | But the earlier commenter indicated that they are very
               | different. So, unfortunately, you have again failed to
               | break the cycle. We need something dumbed down for us
               | regular people, not something directed at those who walk,
               | talk, and sleep Rust.
        
               | tubthumper8 wrote:
               | Sorry to have disappointed you. I don't walk, talk, or
               | sleep either Rust or Go, but was trying to provide some
               | resources to help in case you hadn't seen them yet.
               | 
               | One difference I noticed in the docs is in the Go
               | Reference it says the "go" line of the "go.mod" has to be
               | greater than or equal to the go line of all that modules
               | dependencies, if the go line is 1.21 or higher, so a
               | module for 1.21 can't depend on a module for 1.22 [1]
               | 
               | That restriction doesn't apply for Rust, a library using
               | the 2015 edition can use a dependency that uses the 2018
               | edition, for example.
               | 
               | That's just one difference I noticed in the
               | implementation. The goals seem very similar if not the
               | same
               | 
               | [1] https://go.dev/doc/modules/gomod-ref#go
        
               | 9rx wrote:
               | Thanks for trying. But it is the _" which is very
               | different to what go has now settled on"_ that we are
               | trying to get to the bottom of. It appears from your
               | angle that you also conclude that Go has settled on the
               | very same thing, frivolous implementation details aside.
               | Hopefully phire will still return to dumb it down for us.
        
               | cesarb wrote:
               | I'd say Rust editions are more like going from Python 2.x
               | to Python 2.y, than the 2->3 migration. The Rust standard
               | library is still the same (and the string type is still
               | "valid UTF-8") no matter the edition (this is why you can
               | mix-and-match editions), the edition differences are
               | mostly on the syntax.
               | 
               | > Just imagine how much smoother the python 3 transition
               | would have been if you could transition projects
               | incrementally, module by module as needed.
               | 
               | That would require manually converting strings to the
               | correct type at each module boundary (you can't do it
               | automatically, because on the Python 2.x side, you don't
               | know whether or not a string has already been
               | decoded/encoded into a specific character encoding; that
               | is, you don't know whether a Python 2.x string should be
               | represented by a "bytes" or a "str" on the Python 3.x
               | side). That's made even harder by Python's dynamic typing
               | (you can't statically look at the code and point all the
               | places which might need manual review).
        
             | jerf wrote:
             | The major features that may have required a 2.0 were
             | implemented in a backwards-compatible way, removing the
             | utility of a Go 2.0.
             | 
             | Go 2.0 was basically a blank check for the future that said
             | "We may need to break backwards compatibility in a big
             | way". It turns out the Go team does not see the need to
             | cash that check and there is no anticipated upcoming
             | feature in the next several years that would require it.
             | 
             | The last one that I was sort of wondering about was the
             | standard library, but the introduction of math/rand/v2 has
             | made it clear the devs are comfortable ramping standard
             | library packages without a Go 2. There are a number of
             | standard libraries that I think could stand to take a v2;
             | there aren't any that are so broken that it's worth a v2 to
             | hard-remove them. (Except arguably syscall [1], which turns
             | out it doesn't belong in the standard library because it
             | can't maintain the standard library backwards compatibility
             | and should have been in the extended standard library from
             | the beginning, but that's been the way it is now for a long
             | time and also doesn't rate a v2.)
             | 
             | (And again let me underline I'm not saying all the standard
             | library is perfect. There is some brokenness here and
             | there, for various definitions of "brokenness". I'm just
             | saying it's not _so_ broken that it 's worth a v2 hard
             | break at the language level and hard elimination of the
             | libraries such that old code is forcibly broken and forced
             | to update to continue on.)
             | 
             | [1]: https://pkg.go.dev/syscall
        
       | mrkeen wrote:
       | > If the Go language ever comes to the point where I'd have to
       | write this                 n, err := r.Read(context.TODO(), p)
       | 
       | > put a bullet in my head, please.
       | 
       | Manually passing around a context everywhere sounds about as
       | palatable as manually checking every return for error.
        
         | the_gipsy wrote:
         | Exactly, the snippets needs at least three lines of inane error
         | checking boilerplate and variable juggling.
        
       | the_gipsy wrote:
       | > This probably doesn't happen often, but it's prone to name
       | collisions.
       | 
       | It's funny, it really was just using strings as keys until quite
       | recently, and obviously there were collisions and there was no
       | way to "protect" a key/value, etc.
       | 
       | Now the convention is to use a key with a private type, so no
       | more collisions. The value you get is still untyped and needs to
       | be cast, though. Also there are still many older libraries still
       | uses strings.
        
         | grose wrote:
         | The blog post from 2014 introducing context uses a private key
         | type, so there's really no excuse:
         | https://go.dev/blog/context#package-userip
        
       | pluto_modadic wrote:
       | new solution should be: Simple and elegant. Optional, non-
       | intrusive and non-infectious. Robust and efficient. Only solves
       | the cancelation problem.
       | 
       | okay... so they dodged the thing I thought was going to be
       | interesting, how would you solve passing state? e.g. if I write a
       | middleware for net/http, I have to duplicate the entire
       | http.Request, and add my value to it.
        
       | mukunda_johnson wrote:
       | > It's very similar to thread-local storage. We know how bad of
       | an idea thread-local storage is. Non-flexible, complicates usage,
       | composition, testing.
       | 
       | I kind of do wish we had goroutine local storage though :)
       | Passing down the context of the request everywhere is ugly.
        
         | rednafi wrote:
         | Goroutines have a tiny stack at the beginning, 4KB iirc. Having
         | a goroutine-local storage will probably open a can of worms
         | there.
        
         | PaulKeeble wrote:
         | Context's spread just like exceptions do, the moment you
         | introduce one it flies up and down all the functions to get
         | where it needs to be. I can't help but think that local storage
         | and operations for Go just like Threads have in Java would be a
         | cleaner solution to the problem.
        
         | arccy wrote:
         | now your stuff breaks when you pass messages between channels
        
         | kflgkans wrote:
         | I like explicit over implicit. I will take passing down context
         | (in the sense of the concept, not the specific Go
         | implementation) explicitly everywhere over implicit ("put it
         | somewhere and I'll trust I can [probably, hopefully] get it
         | back later") any day of the week.
         | 
         | I've seen plenty of issues in Java codebases where there was an
         | assumption some item was in the Thread Local storage (e.g. to
         | add some context to a log statement or metric) and it just
         | wasn't there (mostly because code switched to a different
         | thread, sometimes due to a "refactor" where stuff was renamed
         | in one place but not in another).
        
       | rednafi wrote:
       | This article is from 2017!
       | 
       | As others have already mentioned, there won't be a Go 2. Besides,
       | I really don't want another verbose method for cancellation;
       | error handling is already bad enough.
        
         | incognito124 wrote:
         | I thought go 2 was considered harmful
        
           | rednafi wrote:
           | Oh, don't even start about Go's knack for being pithy to a
           | fault.
        
           | TeMPOraL wrote:
           | Yes, that's why you should instead use "COMEFROM", or it's
           | more general form, "LET'S HAVE A WALK".
        
           | robertlagrant wrote:
           | I came here to say this.
        
       | bheadmaster wrote:
       | Contexts in Go are generally used for convenience in request
       | cancellation, but they're not _required_ , and they're not the
       | only way to do it. Under the hood, a context is just a channel
       | that's closed on cancellation. The way it was done before
       | contexts was pretty much the same:                   func
       | CancellableOp(done chan error /* , args... */) {             for
       | {                 // ...                      // cancellable
       | code:                 select {                     case
       | <-something:                         // ...
       | case err := <-done:                         // log error or
       | whatever                 }             }         }
       | 
       | Some compare context "virus" to async virus in languages that
       | bolt-on async runtime on top of sync syntax - but the main
       | difference is you can compose context-aware code with context-
       | oblivious code (by passing context.Background()), and vice versa
       | with no problems. E.g. here's a context-aware wrapper for the
       | standard `io.Reader` that is completely compatible with
       | `io.Reader`:                   type ioContextReader struct {
       | io.Reader             ctx context.Context         }
       | func (rc ioContextReader) Read(p []byte) (n int, err error) {
       | done := make(chan struct{})             go func() {
       | n, err = rc.Reader.Read(p)                 close(done)
       | }()                  select {             case <-rc.ctx.Done():
       | return 0, rc.ctx.Err()             case <-done:
       | return n, err             }         }              func main() {
       | ctx, cancel := context.WithTimeout(context.Background(),
       | 3*time.Second)             defer cancel()                  rc :=
       | ioContextReader{Reader: os.Stdin, ctx: ctx}                  //
       | we can use rc in io.Copy as it is an io.Reader             _, err
       | := io.Copy(os.Stdout, rc)             if err != nil {
       | log.Println(err)             }         }
       | 
       | For io.ReadCloser, we could call `Close()` method when context
       | exits, or even better, with `context.AfterFunc(ctx, rc.Close)`.
       | 
       | Contexts definitely have flaws - verbosity being the one I hate
       | the most - but having them behave as ordinary values, just like
       | errors, makes context-aware code more understandable and
       | flexible.
       | 
       | And just like errors, having cancellation done automatically
       | makes code more prone to errors. When you don't put "on-cancel"
       | code, your code gets cancelled but doesn't clean up after itself.
       | When you don't select on `ctx.Done()` your code doesn't get
       | cancelled at all, making the bug more obvious.
        
         | bryancoxwell wrote:
         | This works, but goes against convention in that (from the
         | context package docs) you shouldn't "store Contexts inside a
         | struct type; instead, pass a Context explicitly to each
         | function that needs it."
        
           | bheadmaster wrote:
           | True. But this code is only proof-of-concept of how non-
           | context-aware functions can be wrapped in a context. Such
           | usage of context is not standard.
        
           | dfawcus wrote:
           | It does seem an unnecessarily limiting convention.
           | 
           | What will go wrong if one stores a Context in a struct?
           | 
           | I've done so for a specific use case, and did not notice any
           | issues.
        
         | kiitos wrote:
         | You're spawning a goroutine per Read call? This is pretty
         | bonkers inefficient, to start, and a super weird approach in
         | any case...
        
           | bheadmaster wrote:
           | Yes, but this is just proof of concept. For any given case,
           | you can optimize your approach to your needs. E.g. single
           | goroutine ReadCloser:                   type
           | ioContextReadCloser struct {             io.ReadCloser
           | ctx context.Context                  ch chan *readReq
           | }              type readReq struct {             p   []byte
           | n   *int             err *error             m   sync.Mutex
           | }              func NewIoContextReadCloser(ctx
           | context.Context, rc io.ReadCloser) *ioContextReadCloser {
           | rcc := &ioContextReadCloser{                 ReadCloser: rc,
           | ctx:        ctx,                      ch: make(chan
           | *readReq),             }             go rcc.readLoop()
           | return rcc         }              func (rcc
           | *ioContextReadCloser) readLoop() {             for {
           | select {                 case <-rcc.ctx.Done():
           | return                 case req := <-rcc.ch:
           | *req.n, *req.err = rcc.ReadCloser.Read(req.p)
           | if *req.err != nil {                         req.m.Unlock()
           | return                     }
           | req.m.Unlock()                 }             }         }
           | func (rcc *ioContextReadCloser) Read(p []byte) (n int, err
           | error) {             req := &readReq{p: p, n: &n, err: &err}
           | req.m.Lock() // use plain mutex as signalling for efficiency
           | select {             case <-rcc.ctx.Done():
           | return 0, rcc.ctx.Err()             case rcc.ch <- req:
           | }             req.m.Lock() // wait for readLoop to unlock
           | return n, err         }
           | 
           | Again, this is not to say this is the _right_ way, only that
           | it is _possible_ and does not require any shenanigans that
           | e.g. Python needs when dealing with when mixing sync  &
           | async, or even different async libraries.
        
             | orf wrote:
             | A mutex in a hot Read (or any IO) path isn't efficient.
        
               | bheadmaster wrote:
               | What would you suggest as an alternative?
        
         | the_gipsy wrote:
         | Consider this:                   ctx, cancel :=
         | context.WithTimeout(context.Background(), 60*time.Second)
         | reader := ioContextReader(ctx, r)         ...         ctx,
         | cancel := context.WithTimeout(ctx, 1*time.Second)         ctx =
         | context.WithValue(ctx, "hello", "world")         ...
         | func(ctx context.Context) {             reader.Read() // does
         | not time out after one second, does not contain hello/world.
         | ...         }(ctx)
        
           | bheadmaster wrote:
           | There are two solutions, depending on your real use case:
           | 
           | 1) You're calling Read() directly and don't need to use
           | functions that strictly accept io.Reader - then just
           | implement ReadContext:                   func (rc
           | ioContextReader) ReadContext(ctx context.Context, p []byte)
           | (n int, err error) {             done := make(chan struct{})
           | go func() {                 n, err = rc.Reader.Read(p)
           | close(done)             }()                  select {
           | case <-ctx.Done():                 return 0, ctx.Err()
           | case <-done:                 return n, err             }
           | }
           | 
           | Otherwise, just wrap the ioContextReader with another
           | ioContextReader:                   reader =
           | ioContextReader(ctx, r)
        
             | the_gipsy wrote:
             | Changing the interface 1) is obviously not relevant.
             | 
             | Re-wrapping works only for the toy example. In the real
             | world, the reader isn't some local variable, but there
             | could be many, across different structs, behind private
             | fields.
             | 
             | To cirle back, and not focus too much on the io.Reader
             | example: the virality of ctx is real, and making wrapper
             | structs is not a good solution. Updating stale references
             | may not be possible, and would quickly become overwhelming.
             | Not to forget the performance overhead.
             | 
             | Personally I think it's okay, go is fine as a "webservices"
             | language. The go gospel is, You can have your cake and eat
             | it too, but it's almost never true unless you twist the
             | meaning of "cake" and "eat".
        
         | kbolino wrote:
         | You are half right. A context also carries a deadline. This is
         | important for those APIs which don't allow asynchronous
         | cancellation but which do support timeouts as long as they are
         | set up in advance. Indeed, your ContextReader is not safe to
         | use in general, as io.ReadCloser does not specify the effect of
         | concurrent calls to Close during Read. Not all implementations
         | allow it, and even when they do tolerate it, they don't always
         | guarantee that it interrupts Read.
        
       | captainmuon wrote:
       | This is about an explicit argument of type "Context". I'm not a
       | Go user, and at first I thought it was about something else: an
       | implicit context variable that allows you to pass stuff deep down
       | the call stack, without intermediate functions knowing about it.
       | 
       | React has "Context", SwiftUI has "@Environment", Emacs LISP has
       | dynamic scope (so I heard). C# has AsyncLocal, Node.JS
       | AsyncLocalStorage.
       | 
       | This is one of those ideas that at first seem really wrong (isn't
       | it just a global variable in disguise?) but is actually very
       | useful and can result in cleaner code with _less_ globals or less
       | superfluous function arguments. Imagine passing a logger like
       | this, or feature flags. Or imagine setting  "debug = True" before
       | a function, and it applies to everything down the call stack (but
       | not in other threads/async contexts).
       | 
       | Implicit context (properly integrated into the type system) is
       | something I would consider in any new language. And it might also
       | be a solution here (altough I would say such a "clever" and
       | unusual feature would be against the goals of Go).
        
         | crowcountry wrote:
         | Scala has implicit contextual parameters: https://docs.scala-
         | lang.org/tour/implicit-parameters.html.
        
           | agumonkey wrote:
           | I've always been curious about how this feature ends up in
           | day to day operations and long term projects. You're happy
           | with it ?
        
             | kloop wrote:
             | As a veteran of a large scala project (which was re-written
             | in go, so I'm not unbiased), no. I was generally not happy.
             | 
             | This was scala 2, so implicit resolution lookup was a big
             | chunk of the problem. There's nothing at the call site that
             | tells you what is happening. But even when it wasn't hidden
             | in a companion object somewhere, it was still difficult
             | because every import change had to be scrutinized as it
             | could cause large changes in behavior (this caused a non-
             | zero number of production issues).
             | 
             | They work well for anything you would use environment
             | variables for, but a chunk of the ecosystem likes to use
             | them for handlers (the signature being a Functor
             | generally), which was painful
        
               | agumonkey wrote:
               | thanks a lot for your answer
        
               | lmm wrote:
               | > There's nothing at the call site that tells you what is
               | happening.
               | 
               | A decent IDE highlights it at the call site.
               | 
               | It's definitely an abusable feature, but I find it very
               | useful. In most other languages you end up having to have
               | completely invisible parameters (e.g. database session
               | bound to the thread) because it would be too cumbersome
               | to pass them explicitly. In Scala you have a middle
               | ground option between completely explicit and completely
               | invisible.
        
               | agumonkey wrote:
               | yeah that's what i thought, but maybe scala implicit
               | param not being perfect will help finding a better
               | linguistic trait (maybe they should enforce purity on
               | these parameters)
        
               | kloop wrote:
               | I'm not sure what you consider a decent scala ide, but it
               | was a problem with IntelliJ in several of our code bases,
               | and I'd have to crawl the implicit resolution path.
               | 
               | I eventually opted to desugaring the scala completely,
               | but we were already on the way out of scala by that point
        
             | xmodem wrote:
             | Not OP, but I briefly seconded to a team that used Scala at
             | a big tech co and I was often frustrated by this feature
             | specifically. They had a lot of code that consumed implicit
             | parameters that I was trying to call from contexts they
             | were not available.
             | 
             | Then again I guess it's better than a production outage
             | because the thread-local you didn't know was a requirement
             | wasn't available.
        
           | segfaltnh wrote:
           | Scala has everything, and therefore nothing.
        
         | sghiassy wrote:
         | In Jetpack compose, the Composer is embedded by the compiler at
         | build time into function calls
         | 
         | https://medium.com/androiddevelopers/under-the-hood-of-jetpa...
         | 
         | I'm still not sure how I feel about it. While more annoying, I
         | think I'd like to see it, rather than just have magic behind
         | the hood
        
           | fragmede wrote:
           | seeing it is great. coming into a hairy monolith and having
           | to plumb one variable through half a dozen layers to get to
           | the creamy nougat later you actually wanted it in, is not.
           | having to do that more than once it's why they invented the
           | "magic" implicit context variable.
        
         | kgeist wrote:
         | Passing the current user ID/tenant ID inside ctx has been super
         | useful for us. We're already using contexts for cancellation
         | and graceful termination, so our application-layer functions
         | already have them. Makes sense to just reuse them to store user
         | and tenant IDs too (which we pull from access tokens in the
         | transport layer).
         | 
         | We have DB sharding, so the DB layer needs to figure out which
         | shard to choose. It does that by grabbing the user/tenant ID
         | from the context and picking the right shard. Without contexts,
         | this would be way harder--unless we wanted to break
         | architecture rules, like exposing domain logic to DB details,
         | and it would generally just clutter the code (passing tenant ID
         | and shard IDs everywhere). Instead, we just use the "current
         | request context" from the standard lib that can be passed
         | around freely between modules, with various bits extracted from
         | it as needed.
         | 
         | What's the alternatives, though? Syntax sugar for retrieving
         | variables from some sort of goroutine-local storage? Not good,
         | we want things to be explicit. Force everyone to roll their own
         | context-like interfaces, since a standard lib's implementation
         | can't generalize well for all sitiations? That's exactly why
         | contexts we introduced--because nobody wanted to deal with
         | mismatched custom implementations from different libs. Split it
         | into separate "data context" and "cancellation context"? Okay,
         | now we're passing around two variables instead of one in every
         | function call. DI to the rescue? You can hide userID/tenantID
         | with clever dependency injection, and that's what we did before
         | we introduced contexts to our codebase, but that resulted in
         | allocations of individual dependency trees for each request
         | (i.e. we embedded userID/tenantID inside request-specific
         | service instances, to hide the current userID/tenantID, and
         | other request details, from the domain layer to simplify domain
         | logic), and it stressed the GC.
        
           | vbezhenar wrote:
           | An alternative is to add all dependencies explicitly into
           | function argument list or object fields, instead of using
           | them implicitly from the context, without documentation and
           | static typing. Including logger.
        
             | kgeist wrote:
             | I already talked about it above.
             | 
             | Main problems with passing dependencies in function
             | argument lists:
             | 
             | 1) it pollutes the code and makes refactoring harder (a
             | small change in one place must be propagated to all call
             | sites in the dependency tree which recursively accept user
             | ID/tenant ID and similar info)
             | 
             | 2) it violates various architectural principles, for
             | example, from the point of view of our business logic,
             | there's no such thing as "tenant ID", it's an
             | implementation detail to more efficiently store data, and
             | if we just rely on function argument lists, then we'd have
             | to litter actual business logic with various
             | infrastructure-specific references to tenant IDs and the
             | like so that the underlying DB layer could figure out what
             | to do.
             | 
             | Sure, it can be solved with constructor-based dependency
             | injection (i.e. request-specific service instances are
             | generated for each request, and we store user ID/tenant ID
             | & friends as object fields of such request-scoped
             | instances), and that's what we had before switching to
             | contexts, but it resulted in excessive allocations and
             | unnecessary memory pressure for our highload services. In
             | complex enterprise code, those dependency trees can be
             | quite large -- and we ended up allocating huge dependency
             | trees for each request. With contexts, we now have a single
             | application-scoped service dependency tree, and request-
             | specific stuff just comes inside contexts.
             | 
             | Both problems can be solved by trying to group and reuse
             | data cleverly, and eventually you'll get back to square one
             | with an implementation which looks similar to ctx.Context
             | but which is not reusable/composable.
             | 
             | >Including logger.
             | 
             | We don't store loggers in ctx, they aren't request-
             | specific, so we just use constructor-based DI.
        
               | TeMPOraL wrote:
               | I believe this problem isn't solvable under our current
               | paradigm of programming, which I call "working directly
               | on plaintext, single-source-of-truth codebase".
               | 
               | Tenant ID, cancellations, loggers, error handling are all
               | examples of cross-cutting concerns. Depending on what any
               | given function does, and what _you_ (the programmer) are
               | interested in at a given moment, any of them could be
               | critical information or pure noise. Ideally, you should
               | not be seeing the things you don 't care about, but our
               | current paradigm forces us to spell out _all of them, at
               | all times_ , hurting readability and increasing
               | complexity.
               | 
               | On the readability/"clean code", our most advanced
               | languages are operating on a Pareto frontier. We have
               | whole math fields being employed in service of packaging
               | up common cross-cutting concerns, as to minimize the
               | noise they generate. This is where all the magic monads
               | come from, this is why you have to pay attention to
               | infectious colors of your functions, etc. Different
               | languages make slightly different trade-offs here, to
               | make some concerns more readable, but since it's a Pareto
               | frontier, it always makes some other aspects of code
               | _less_ comprehensible.
               | 
               | In my not so humble opinion, we won't progress beyond
               | this point until we give up on the paradigm itself. We
               | need to accept that, at any given moment, a programmer
               | may need a different perspective on the code, and we need
               | to build tools to allow writing code from those
               | perspectives. What we now call source code should be
               | relegated to the role of intermediary/object code - a
               | single source of truth _for the bowels of the compiler_ ,
               | but otherwise something we never touch directly.
               | 
               | Ultimately, the problem of "context" is a problem of
               | perspective, and should be solved by tooling. That is,
               | when reading or modifying code, I should be able to
               | ignore any and all context I don't care about. One
               | moment, I might care about the happy path, so I should be
               | able to view and edit code with all error propagation
               | removed; at another moment, I might care about how all
               | the data travels through the module, in which case I want
               | to see _the same code_ with every single goddamn thing
               | spelled out explicitly, in the fashion GP is arguing to
               | be the default. Etc.
               | 
               | Plaintext is fine. Single source of truth is fine. A
               | single all-encompassing view of everything in a source
               | file is fine. But they're not fine all together, all the
               | time.
        
               | xyz-x wrote:
               | Monads but more importantly MonadTransformers so you can
               | program in a legible fashion.
               | 
               | However, there's a lot of manual labour to stuff
               | everything into a monad, and then extract it and pattern
               | match when your libraries don't match your choice of
               | control flow monad(s)!
               | 
               | This is where I'd prefer if compilers could come in.
               | 
               | Imagine being in the bowels of a DB lib, and realising
               | that the function you just write might be well positioned
               | to terminate the TCP connection that it's using to talk
               | to the database with. Oh no: now you have to update the
               | signature and every single call-site for its parent, and
               | its parent, and...
               | 
               | Instead, it would be neat if the compiler could treat
               | things you deem cross-cutting as a graph traversal
               | problem instead; call a cancelable method and all callers
               | are automatically cancelable. Decisions about whether to
               | spawn a cancelable subtree, to 'protect' some execution
               | or set a deadline is then written on an opt-in basis per
               | function; all functions compose. The compiler can
               | visualise the tree of cancellation (or hierachical
               | loggers, or OT spans, or actors, or green fibers, or ...)
               | and it can enforce the global invariant that the entry-
               | point captures SIGINT (or sets up logging, or sets up a
               | tracer, or ...).
               | 
               | So imagine the infrastructure of a monad transformer, but
               | available per-function on an opt-in basis. If you write
               | your function to have a cleanup on cancellation, or write
               | logs around any asynchronous barrier, the fiddly details
               | of stuffing the monad is done by the compiler and
               | optionally visualised and explained in the IDE. Your code
               | doesn't have to opt-in, so you can make each function
               | very clean.
        
               | TeMPOraL wrote:
               | Yes, there's plenty of space for automation and advanced
               | support from tooling. Hell, not every perspective is best
               | viewed as plaintext; in particular, anything that looks
               | like a directed graph fundamentally cannot be well-
               | represented in plaintext _at all_ without repeating
               | nodes, breaking the 1:1 correspondence between a token
               | and a thing represented by that token.
               | 
               | Still, I believe the core insight here is that we need
               | different perspectives at different times. Using your
               | example, most of the time I probably _don 't care_
               | whether the code is cancellable or not. Any mention of it
               | is _distracting noise_ to me. But other times - perhaps
               | next day, or perhaps just five minutes later, I suddenly
               | _need_ to know whether the code is cancellable, and
               | perhaps I need to explicitly opt out of it somewhere. It
               | 's highly likely that in those cases, I may not care
               | about things like error handling logic and passing around
               | session identifiers, and I would like _that_ to disappear
               | in those moments, etc.
               | 
               | And hell, I might need an overview of the which code is
               | or isn't protected, and that would be best served by
               | showing me an interactive DAG of functions that I can
               | zoom around and expand/collapse, so that's _another_ kind
               | of perspective. Etc.
               | 
               | EDIT:
               | 
               | And then there's my favorite example: the unending holy
               | war of "few fat functions" vs. "lots of tiny functions".
               | Despite the endless streams of Tweets and articles
               | arguing for either, there is _no right choice_ here -
               | there 's no right trade-off you can make here _up front_
               | , and can never be, because which one is more readable
               | depends strictly on _why you 're reading it_. E.g. lots
               | of tiny functions reduce duplication and can introduce a
               | language you can use to effectively think about some code
               | at a higher level - but if there's a thorny bug in there
               | I'm trying to fix, _I want all of that shit inlined into
               | one, big function, that I can step through sequentially_
               | , following the actual execution order.
               | 
               | It is my firm belief that the ability to inline and
               | uninline code on the fly, for yourself, personally,
               | without affecting the actual execution or the work of
               | other developers, is one of the most important missing
               | piece in our current tooling, and making it happen is a
               | good first step towards abandoning The Current Paradigm
               | that is now suffocating us all.
               | 
               | Second one would be, along with inlining, the ability to
               | just give variables and parameters fixed values when
               | reading, and have those values be displayed and
               | propagated through the code - effectively doing a partial
               | simulation of code execution. Being able to do it ad hoc,
               | temporarily, would be a huge aid in quickly understanding
               | what some code does.
        
               | jauntywundrkind wrote:
               | Promises are so incredibly close to being a
               | representation of work.
               | 
               | The OS has such sophisticated tools for process
               | management, but inside a process there are so many
               | subprocesses going on, & it feels like we are flailing
               | about with poorly managed process like things. (Everyone
               | except Erlang.)
               | 
               | I love how close zx comes to touching the sky here. It's
               | a typescript library for running processes, as a tagged
               | template function returning a promise. *const hello =
               | $`sleep 3; echo hello world`. But the promise isnt just a
               | "a future value", it is A ProcessPromise for interacting
               | with the promise.
               | 
               | I so wish promises were just a little better. It feels
               | like such a bizarre tragedy to me the "a promise is a
               | future value" not a thing unto itself won the day in es6
               | / es2015, destroyed the possibility of a promise being
               | more; zx has run into a significant number of ergonomic
               | annoyances because this small world dogma.
               | 
               | How cool it would be to see this go further. I'd love for
               | the language to show what promises if any this promise is
               | awaiting! I long for that dependency graph of
               | subprocesses to start to show itself, not just at compile
               | time but for the runtime to be able to actively observe
               | and manage the subprocesses within it at runtime. We keep
               | building workflow engines, build robust userland that
               | manage their own subprocesses, user user lands, but the
               | language itself seems so close & yet so far from letting
               | the simple promise become more a process, and that seems
               | like a sad shame.
        
               | youerbt wrote:
               | > it violates various architectural principles, for
               | example, from the point of view of our business logic,
               | there's no such thing as "tenant ID"
               | 
               | I'm not sure I understand how hiding this changes
               | anything. Could you just not pass "tenant ID" to
               | doBusinessLogic function and pass it to saveToDatabase
               | function?
        
               | tubthumper8 wrote:
               | That's exactly what what they're talking about,
               | "tenantId" shouldn't be in the function signature for
               | functions that aren't concerned with the tenant ID, such
               | as business logic
        
               | giancarlostoro wrote:
               | I have a feeling, if Context disappears, you'll just see
               | "Context" becoming a common struct that is passed around.
               | In Python, unlike in C# and Java, the first param for a
               | Class Method is usually the class instance itself, it is
               | usually called "self" so I could see this becoming the
               | norm in Go.
        
               | neonsunset wrote:
               | Under the hood, in both Java and C# the first argument of
               | an instance method is the instance reference itself.
               | After all, instance methods imply you have an instance to
               | work with. Having to write 'this' by hand for such is how
               | OOP was done before OOP languages became a thing.
               | 
               | I agree that adopting yet another pattern like this would
               | be on brand for Go since it prizes taking its opinionated
               | way of going about everything in a vintage kind of way
               | over being practical and convenient.
        
               | danudey wrote:
               | As a newcomer to Go, a lot of their design decisions made
               | a lot of sense when I realized that a lot of the design
               | is based around this idea of "make it impossible to do
               | something that could be dumb in some contexts".
               | 
               | For example, I hate that there's no inheritance. I wish I
               | could create a ContainerImage object and then a
               | RemoteContainerImage subclass and then QuayContainerImage
               | and DockerhubContainerImage subclasses from those.
               | However, being able to do inheritance, and especially
               | multiple inheritance, can lead to awful, idiotic code
               | that is needlessly complicated for no good reason.
               | 
               | At a previous job we had a script that would do
               | operations on a local filesystem and then FTP items to a
               | remote. I thought okay, the fundamental paradigms of FTP
               | and SFTP-over-SSH via the paramiko module are basically
               | identical so it should be a five minute job to patch it
               | in, right?
               | 
               | Turns out this Python script, which, fundamentally,
               | consisted of "take these files here and put them over
               | there" was the most overdesigned piece of garbage I've
               | ever seen. Clean, effective, and entirely functional
               | code, but almost impossible to reason about. The code
               | that did the actual work was six classes and multiple
               | subclasses deep, but assumptions were baked in at every
               | level. FTP-specific functionality which called a bunch of
               | generic functionality which then called a bunch of FTP-
               | specific functionality. In order to add SFTP support I
               | would have had to effectively rewrite 80% of the code
               | because even the generic stuff inherited from the FTP-
               | specific stuff.
               | 
               | Eventually I gave up entirely and just left it alone; it
               | was too important a part of a critical workflow to risk
               | breaking and I never had the time or energy to put my
               | frustration aside. Golang, for all its flaws, would have
               | prevented a lot of that because a lot of the self-
               | gratification this programmer spent his time on just
               | wouldn't have been possible in Go for exactly this
               | reason.
        
             | cle wrote:
             | > instead of using them implicitly from the context,
             | without documentation and static typing
             | 
             | This is exactly what context is trying to avoid, and makes
             | a tradeoff to that end. There's often intermediate business
             | logic that shouldn't need to know anything about logging or
             | metrics collection or the authn session. So we stuff things
             | into an opaque object, whether it's a map, a dict, a magic
             | DI container, "thread local storage", or whatever. It's a
             | technique as old as programming.
             | 
             | There's nothing preventing you from providing well-typed
             | and documented accessors for the things you put into a
             | context. The context docs themselves recommend it and
             | provide examples.
             | 
             | If you disagree that this is even a tradeoff worth making,
             | then there's not really a discussion to be had about _how_
             | to make it.
        
               | vbezhenar wrote:
               | I disagree that it's a good approach. I think that
               | parameters must be passed down always, as parameters. It
               | allows compiler to detect unused parameters and it
               | removes all implicitness.
               | 
               | It is verbose indeed and may be there should be
               | programming language support to reduce that verbosity.
               | Some languages support implicit parameters which proved
               | to be problematic but may be there should be more
               | iterations on that manner.
               | 
               | I consider context for passing down values to do more
               | harm than good.
        
               | cle wrote:
               | It's nothing to do with verbosity, which is why I didn't
               | mention it.
        
             | danudey wrote:
             | Other responses cover this well, but: the idea of having to
             | change 20 functions to accept and propagate a `user` field
             | just so that my database layer can shard based on userid is
             | gross/awful.
             | 
             | ...but doing the same with a context object is also
             | gross/awful.
        
         | TeMPOraL wrote:
         | > _React has "Context", SwiftUI has "@Environment", Emacs LISP
         | has dynamic scope (so I heard). C# has AsyncLocal, Node.JS
         | AsyncLocalStorage._
         | 
         | Emacs Lisp retains dynamic scope, but it's no longer a default
         | for some time now, in line in other Lisps that remain in use.
         | Dynamic scope is one of the greatest features in Lisp language
         | family, and it's sad to see it's missing almost everywhere else
         | - where, as you noted, it's being reinvented, but poorly,
         | because it's not a first-class language feature.
         | 
         | On that note, the most common case of dynamic scope that almost
         | everyone is familiar with, are _environment variables_. That 's
         | what they're for. Since most devs these days are not familiar
         | with the idea of dynamic scope, this leads to a lot of peculiar
         | practices and footguns the industry has around environment
         | variables, that all stem from misunderstanding what they are
         | for.
         | 
         | > _This is one of those ideas that at first seem really wrong
         | (isn 't it just a global variable in disguise?)_
         | 
         | It's not. It's about scoping a value _to the call stack_.
         | Correctly used, rebinding a value to a dynamic variable should
         | only be visible to the block doing the rebinding, and
         | everything below it on the call stack at runtime.
         | 
         | > _Implicit context (properly integrated into the type system)
         | is something I would consider in any new language._
         | 
         | That's the problem I believe is currently unsolved, and
         | possibly unsolvable in the overall programming paradigm we work
         | under. One of the main practical benefits of dynamic scope is
         | that place X can set up some value for place Z down on the call
         | stack, while keeping everything in between X and Z oblivious of
         | this fact. Now, this is trivial in dynamically typed language,
         | but it goes against the principles behind statically-typed
         | languages, which all _hate_ implicit things.
         | 
         | (FWIW, I love types, but I also hate having to be explicit
         | about irrelevant things. Since whether something is relevant or
         | not isn't just a property of code, but also a property of a
         | specific programmer at specific time and place, we're in a bit
         | of a pickle. A shorter name for "stuff that's relevant or not
         | depending on what you're doing at the moment" is _cross-cutting
         | concerns_ , and we still suck at managing them.)
        
           | masklinn wrote:
           | > Emacs Lisp retains dynamic scope, but it's no longer a
           | default for some time now
           | 
           | https://www.gnu.org/software/emacs/manual/html_node/elisp/Va.
           | ..
           | 
           | > By default, the local bindings that Emacs creates are
           | dynamic bindings. Such a binding has dynamic scope, meaning
           | that any part of the program can potentially access the
           | variable binding. It also has dynamic extent, meaning that
           | the binding lasts only while the binding construct (such as
           | the body of a let form) is being executed.
           | 
           | It's also not really germane to the GP's comment, as they're
           | just talking about dynamic scoping being available, which it
           | will almost certainly always be (because it's useful).
        
             | TeMPOraL wrote:
             | Sorry, you're right. It's not a _cultural_ default anymore.
             | I.e. Emacs Lisp got proper lexical scope some time ago, and
             | since then, you 're supposed to start every new .elisp file
             | with:                 ;; -*- mode: emacs-lisp; lexical-
             | binding: t; -*-
             | 
             | i.e. explicitly switching the interpreter/compiler to work
             | in lexical binding mode.
        
           | siknad wrote:
           | > against the principles behind statically-typed languages,
           | which all hate implicit things
           | 
           | But many statically typed languages allow throwing exceptions
           | of any type. Contexts can be similar: "try catch" becomes
           | "with value", "throw" becomes "get".
        
             | TeMPOraL wrote:
             | Yes, but then those languages usually implement only
             | unchecked exception, as propagating error types up the call
             | tree is seen as _annoying_. And then, because there are
             | good reasons you may want to have typed error values
             | (instead of just  "any"), there is now pressure to use
             | _result types_ (aka.  "expected", "maybe") instead -
             | turning your return type Foo into Result<Foo, ErrorType>.
             | 
             | And all that it does is making you spell out _the entire
             | exception handling mechanism_ explicitly in your code - not
             | just propagating the types up the call tree, but also
             | making every function explicitly wrapping, unwrapping and
             | branching on Result types. The latter is so annoying that
             | people invent _new syntax_ to hide it - like tacking ? at
             | the end of the function, or whatever.
             | 
             | This becomes even worse than checked exception, but it's
             | apparently what you're supposed to be doing these days, so
             | -\\_(tsu)_/-.
        
               | siknad wrote:
               | We could make explicit effect (context, error)
               | declarations for public functions and inferred for
               | private functions. Explicit enumeration of possible
               | exceptions is required for stable APIs anyway.
        
         | whstl wrote:
         | Yeah, I agree 100% with you. The thing with Golang is that it's
         | supposed to be a very explicit language, so passing the context
         | as an argument fits in with the rest of the language.
         | 
         | Nevertheless: just having it, be it implicit or explicit, beats
         | having to implement it yourself.
        
         | segfaltnh wrote:
         | Thread local storage means all async tasks (goroutines) must
         | run in the same thread. This isn't how tasks are actually
         | scheduled. A request can fan out, or contention can move parts
         | of the computation between threads, which is why context
         | exists.
         | 
         | Furthermore in Go threads are spun up at process start, not at
         | request time, so thread-local has a leak risk or cleanup cost.
         | Contexts are all releasable after their processing ends.
         | 
         | I've grown to be a huge fan of Go for servers and context is
         | one reason. That said, I agree with a lot of the critique and
         | would love to see an in-language solution, but thread-local
         | ain't it.
        
           | cyberax wrote:
           | A more correct term is "goroutine-local" storage, which Go
           | _already_ has. It's used for pprof labels, they are even
           | inherited when a new Goroutine is started.
        
         | karolinepauls wrote:
         | A good pitch for dynamic (context) variables is that they're
         | not globals, they're like implicit arguments passed to all
         | functions within the scope.
         | 
         | Personally I've used the (ugly) Python contextvars for:
         | 
         | - SQS message ID in to allow extending message visibility in
         | any place in the code
         | 
         | - scoped logging context in logstruct (structlog killer in
         | development :D)
         | 
         | I no longer remember what I used Clojure dynvars for, probably
         | something dumb.
         | 
         | That being said, I don't believe that "active" objects like DB
         | connection/session/transaction are good candidates for a
         | context var value. Programmers need to learn to push side
         | effects up the stack instead. Flask-SQLAlchemy is not correct
         | here.
         | 
         | Even Flask's request object being context-scoped is a bad thing
         | since it is usually not a problem to do all the dispatching in
         | the view.
        
         | lmm wrote:
         | > Implicit context (properly integrated into the type system)
         | is something I would consider in any new language.
         | 
         | Those who forget monads are doomed to reinvent dozens of
         | limited single-purpose variants of them as language features.
        
           | mananaysiempre wrote:
           | Algebraic effects and implicit arguments with explicit
           | records are perfectly cromulent language features. GHC
           | Haskell already has implicit arguments, and IIRC Scala uses
           | them instead of a typeclass/trait system. The situation with
           | extensible records in Haskell is more troublesome, but it's
           | more because of the endless bikeshedding of precisely how
           | powerful they should be and because you can get almost all
           | the way there with the existing type-system features except
           | the ergonomics invariably suck.
           | 
           | It's reasonable, I think, to want the dynamic scope but not
           | the control-flow capabilities of monads, and in a language
           | with mutability that might even be a better choice. (Then
           | again, maybe not--SwiftUI is founded on Swift's result
           | builders, and those seem pretty much like monads by another
           | name to me.) And I don't think anybody likes writing the
           | boilerplate you need to layer a dozen MonadReaders or -States
           | on each other and then compose meaningful MonadMyLibraries
           | out of them.
           | 
           | Finally, there's the question of strong typing. You do want
           | the whole thing to be strongly typed, but you don't want the
           | caller to write the entire dependency tree of the callee, or
           | perhaps even to know it. Yet the caller may want to declare a
           | type for itself. Allowing type signatures to be partly
           | specified and partly inferred is not a common feature, and in
           | general development seems to be backing away from large-scale
           | type inference of this sort due to issues with compile
           | errors. Not breaking ABI when the dependencies change
           | (perhaps through default values of some sort) is a more
           | difficult problem still.
           | 
           | (Note the last part can be repeated word for word for checked
           | exceptions/typed errors. Those are also, as far as I'm aware,
           | largely unsolved--and no, Rust doesn't do much here except
           | make the problem more apparent.)
        
         | cesarb wrote:
         | > an implicit context variable that allows you to pass stuff
         | deep down the call stack, without intermediate functions
         | knowing about it. [...] but is actually very useful and can
         | result in cleaner code with less globals or less superfluous
         | function arguments. [...] and it applies to everything down the
         | call stack (but not in other threads/async contexts).
         | 
         | In my experience, these "thread-local" implicit contexts are a
         | pain, for several reasons. First of all, they make refactoring
         | harder: things like moving part of the computation to a thread
         | pool, making part of the computation lazy, calling something
         | which ends up modifying the implicit context behind your back
         | without you knowing, etc. All of that means you have to
         | manually save and restore the implicit context (inheritance
         | doesn't help when the thread doing the work is not under your
         | control). And for that, you have to know which implicit
         | contexts exist (and how to save and restore them), which leads
         | to my second point: they make the code harder to understand and
         | debug. You have to know and understand each and every implicit
         | context which might affect code you're calling (or code called
         | by code you're calling, and so on). As proponents of another
         | programming language would say, explicit is better than
         | implicit.
        
           | mst wrote:
           | They're basically dynamic scoping and it's both a very useful
           | and powerful and very dangerous feature ... scheme's dynamic-
           | wind model makes it more obvious when the particular form of
           | magic is in use but isn't otherwise a lot different.
           | 
           | I would like to think that somebody better at type systems
           | than me could provide a way to encode it into one that
           | doesn't require typing out the dynamic names and types on
           | every single function but can instead infer them based on
           | what other functions are being called therein, but even
           | assuming you had that I'm not sure how much of the (very
           | real) issues you describe it would ameliorate.
           | 
           | I think for golang the answer is probably "no, that sort of
           | powerful but dangerous feature is not what we're going for
           | here" ... and yet when used sufficiently sparingly in other
           | languages, I've found it incredibly helpful.
           | 
           | Trade-offs all the way down as ever.
        
         | flohofwoe wrote:
         | I haven't seen it mentioned yet, but Odin also has an implicit
         | `context` variable:
         | 
         | https://odin-lang.org/docs/overview/#implicit-context-system
        
         | biodniggnj wrote:
         | And Java added ScopedValue in version 20 as a preview feature.
        
         | dang wrote:
         | We added exactly this feature to Arc* and it has proven quite
         | useful. Long writeup in this thread:
         | 
         | https://news.ycombinator.com/item?id=11240681 (March 2016)
         | 
         | * the Lisp that HN is written in
        
       | jbub wrote:
       | 2017!!!
        
       | sir_eliah wrote:
       | > If you use ctx.Value in my (non-existent) company, you're fired
       | 
       | What a nice attitude.
        
       | mickael-kerjean wrote:
       | > If you use ctx.Value in my (non-existent) company, you're fired
       | 
       | I was unsuccessful to convey the same message in my previous
       | company (apart from being fired part). All around the codebase
       | you'd see function with official argument and unofficial ones via
       | ctx that would panic everything if you forgot it was used 3
       | layers down (not kidding). The only use case I've seen so far
       | that is not terrible of context value is if you have a layer of
       | opentelemetry as it makes things transparent and as a caller you
       | don't have to give a damn how the telemetry is operated under the
       | hood.
        
       | rw_panic0_0 wrote:
       | there's no Go 2
        
         | euroderf wrote:
         | "Go 2 Considered Harmful"
        
       | alkonaut wrote:
       | Was this solved? Is this context only a cancellation flag or does
       | it do something more? The obvious solution for a cancellation
       | trigger would be to have cancellation as an optional second
       | argument. That's how it's solved in e.g. C#. Failing to pass the
       | argument just makes it CancellationToken.None, which is simply
       | never cancelled. So I/O without cancellation is simply
       | foo.ReadAsync(x) and with cancellation it's foo.ReadAsync(x, ct).
        
         | whstl wrote:
         | It's not just for cancellation and timeouts, it is also used
         | for passing down metadata, but also for cross-cutting concerns
         | like structured loggers.
        
       | kalekold wrote:
       | > If you use ctx.Value in my (non-existent) company, you're fired
       | 
       | This is such a bad take.
       | 
       | ctx.Value is incredibly useful for passing around context of api
       | calls. We use it a lot, especially for logging such context
       | values as locales, ids, client info, etc. We then use these
       | context values when calling other services as headers so they
       | gain the context around the original call too. Loggers in all
       | services pluck out values from the context automatically when a
       | log entry is created. It's a fantastic system and serves us well.
       | e.g.                   log.WithContext(ctx).Errorf("....", err)
        
         | b1-88er wrote:
         | Maybe he doesn't have a company because he is too dogmatic
         | about things that don't really matter.
        
           | PUSH_AX wrote:
           | 100%
           | 
           | People who have takes like this have likely never zoomed out
           | enough to understand how their software delivery ultimately
           | affects the business. And if you haven't stopped to think
           | about that you might have a bad time when it's your business.
        
           | daviddever23box wrote:
           | Bingo. Everything that can be wrongly used or abused started
           | out its existence within sane constraints and use patterns.
        
         | frankie_t wrote:
         | The author gave a pretty good reasoning why is it a bad idea,
         | in the same section. However, for the demonstration purposes I
         | think the they should have included their vision on how the
         | request scoped data should be passed.
         | 
         | As I understand they propose to pass the data explicitly, like
         | a struct with fields for all possible request-scoped data.
         | 
         | I personally don't like context for value passing either, as it
         | is easy to abuse in a way that it becomes part of the API: the
         | callee is expecting something from the caller but there is no
         | static check that makes sure it happens. Something like passing
         | an argument in a dictionary instead of using parameters.
         | 
         | However, for "optional" data whose presence is not required for
         | the behavior of the call, it should be fine. That sort of
         | discipline has to be enforced on the human level,
         | unfortunately.
        
           | rubenv wrote:
           | > As I understand they propose to pass the data explicitly,
           | like a struct with fields for all possible request-scoped
           | data.
           | 
           | So basically context.Context, except it can't propagate
           | through third party libraries?
        
             | frankie_t wrote:
             | If you use a type like `map[string]any` then yes, it's
             | going to be the same as Context. However, you can make a
             | struct with fields of exactly the types you want.
             | 
             | It won't propagate to the third-party libraries, yes. But
             | then again, why don't they just provide an explicit way of
             | passing values instead of hiding them in the context?
        
               | skywhopper wrote:
               | Precisely because you need to be able to pass it
               | _through_ third party libraries and into callbacks on the
               | other side where you need to recover the values.
        
         | sluongng wrote:
         | Let me try to take the other side:
         | 
         | `ctx.Value` is an `any -> any` kv store that does not come with
         | any documentation, type checking for which key and value should
         | be available. It's quick and dirty, but in a large code base,
         | it can be quite tricky to check if you are passing too many
         | values down the chain, or too little, and handle the failure
         | cases.
         | 
         | What if you just use a custom struct with all the fields you
         | may need to be defined inside? Then at least all the field
         | types are properly defined and documented. You can also use
         | multiple custom "context" structs in different call paths, or
         | even compose them if there are overlapping fields.
        
           | smarkov wrote:
           | > `ctx.Value` is an `any -> any` kv store that does not come
           | with any documentation, type checking for which key and value
           | should be available.
           | 
           | On a similar note, this is also why I highly dislike struct
           | tags. They're string magic that should be used sparingly, yet
           | we've integrated them into data parsing, validation, type
           | definitions and who knows what else just to avoid a bit of
           | verbosity.
        
             | homebrewer wrote:
             | Most popular languages support annotations of one type or
             | another, they let you do all that in a type safe way. It's
             | Go that's decided to be different for difference sake, and
             | produced a complete mess.
        
           | bluetech wrote:
           | > `ctx.Value` is an `any -> any` kv store that does not come
           | with any documentation, type checking for which key and value
           | should be available
           | 
           | The docs https://pkg.go.dev/context#Context suggest a way to
           | make it type-safe (use an unexported key type and provide
           | getter/setter). Seems fine to me.
           | 
           | > What if you just use a custom struct with all the fields
           | you may need to be defined inside?
           | 
           | Can't seamlessly cross module boundaries.
        
           | Thaxll wrote:
           | Because you should wrapp that in a type safe function. You
           | should not use the context.GetValue() directly but use your
           | own function, the context is just a transport mechanism.
        
             | kflgkans wrote:
             | If it is just a transport mechanism, why use context at all
             | ant not a typed struct?
        
           | throw_m239339 wrote:
           | > `ctx.Value` is an `any -> any`
           | 
           | It did not have to be this way, this is a shortcoming of Go
           | itself. Generic interfaces makes things a bit better, but Go
           | designers chose that dumb typing at first place. The std lib
           | is full of interface {} use iteself.
           | 
           | context itself is an after thought, because people were
           | building thread unsafe leaky code on top of http request with
           | no good way to easily scope variables that would scale
           | concurrently.
           | 
           | I remember the web session lib for instance back then, a
           | hack.
           | 
           | ctx.Value is made for each go routine scoped data, that's the
           | whole point.
           | 
           | If it is an antipattern well, it is an antipattern designed
           | by go designers themselves.
        
         | elAhmo wrote:
         | We effectively use this approach in most of our go services.
         | Other than logging purposes, we sometimes use it to pass stuff
         | that is not critical but highly useful to have, like some
         | request and response bodies from HTTP calls, tenant information
         | and similar info.
        
       | miffy900 wrote:
       | > First things first, let's establish some ground. Go is a good
       | language for writing servers, but Go is not a language for
       | writing servers. Go is a general purpose programming language,
       | just like C, C++, Java or Python
       | 
       | Really? Even years later in 2025, this never ended up being true.
       | Unless your definition of 'general purpose' specifically excludes
       | anything UI-related, like on desktop, web or mobile, or AI-
       | related.
       | 
       | I know it's written in 2017, but reading it now in 2025 and
       | seeing the author comparing it to Python of all languages in the
       | context of it's supposed 'general purpose'ness is just laughable.
       | Even Flutter doesn't support go. granted, that seems like a very
       | deliberate decision to justify Dart's existence.
        
         | pjmlp wrote:
         | In an alternative timeline, had Rust 1.0 been available when
         | Docker pivoted away from Python into Go, and Kubernetes from
         | Java into Go, due to having Go folks pushing for the rewrite,
         | and most likely they would have been taken by RIIR instead,
         | nowadays spreading across Python and JavaScript ecosystem,
         | including rewriting tools originally written in Go.
        
           | cyberax wrote:
           | Nope. Rust is not a good tool for servers. It's downright
           | terrible, in fact. Goroutines help _a_ _lot_ with
           | concurrency.
        
         | dlisboa wrote:
         | > Unless your definition of 'general purpose' specifically
         | excludes anything UI-related, like on desktop, web or mobile,
         | or AI-related.
         | 
         | By that definition no language is general purpose. There is no
         | language today that excels in GUI (desktop/mobile), web
         | development, AI, cloud infrastructure, and all the other stuff
         | like systems, embedded...And all at the same time.
         | 
         | For instance I have never seen or heard of a successful Python
         | desktop app (or mobile for that matter).
        
       | theThree wrote:
       | Context is useful in many cases. In go I have to pass ctx from
       | func to func. In nodejs I can easily create&use context by using
       | AsyncLocalStorage (benefit of single-thread).
        
       | nickcw wrote:
       | Contexts implement the idea of cancellation along with go routine
       | local storage and at that they work very well.
       | 
       | What if for the hypothetical Go 2 we add an implicit context for
       | each goroutine. You'd probably need to call a builtin, say
       | `getctx()` to get it.
       | 
       | The context would be inherited by all go routines automatically.
       | If you wanted to change the context then you'd use another
       | builtin `setctx()` say.
       | 
       | This would have the usefulness of the current context without
       | having to pass it down the call chain everwhere.
       | 
       | The cognitive load is two bultins getctx() and setctx(). It would
       | probably be quite easy to implement too - just stuff a
       | context.Context in the G.
        
       | n144q wrote:
       | I find "CancellationToken" in VSCode extension APIs quite clear
       | and usable, and not overly complicated. Wonder if anyone has done
       | a conparison of Go's context and CancellationToken.
        
         | smashedtoatoms wrote:
         | Yeah, .NET developers have been passing CancellationTokens
         | around in the places where they have needed them for 15 years.
         | The tokens are basically invisible until their existence
         | emerges when someone decides they want to cancel a long-running
         | API call or something. At that point, they are plumbed as
         | deeply as seems fit for the problem at hand and then hardly
         | thought about ever again. CancellationTokens are generally a
         | delightful pattern, especially when the language allows
         | sensible defaults.
        
       | skywhopper wrote:
       | I agree so strongly with this piece. Go's context lib is
       | essential, confusing, functional, and should be handled at the
       | language level, but like this author I also have no ideas for
       | what the design should be.
        
       | dang wrote:
       | Discussed at the time:
       | 
       |  _Context should go away for Go 2_ -
       | https://news.ycombinator.com/item?id=14951753 - Aug 2017 (40
       | comments)
        
       | steve_adams_86 wrote:
       | > If you use ctx.Value in my (non-existent) company, you're fired
       | 
       | Yeah, okay. I tried to find reasons you'd want to use this
       | feature and ultimately found that I really, really dislike it.
        
       ___________________________________________________________________
       (page generated 2025-01-21 23:02 UTC)