logoalt Hacker News

bheadmaster01/21/20254 repliesview on HN

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.


Replies

kbolino01/21/2025

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.

bryancoxwell01/21/2025

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.”

show 2 replies
the_gipsy01/21/2025

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)
show 2 replies
kiitos01/21/2025

You're spawning a goroutine per Read call? This is pretty bonkers inefficient, to start, and a super weird approach in any case...

show 1 reply