logoalt Hacker News

bheadmaster01/21/20252 repliesview on HN

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.

Replies

kiitos01/24/2025

I think you're missing the forest for the trees, here.

The io.Reader/Writer interfaces, and their implementations, are meant to provide a streaming model for reading and writing bytes, which is as efficient as reasonably possible, within the constraints of the core language.

If your goal is to make an io.Reader that respects a context.Context cancelation, then you can just do

    type ContextReader struct {
        ctx context.Context
        r   io.Reader
    }

    func NewContextReader(ctx context.Context, r io.Reader) *ContextReader {
        return &ContextReader{
            ctx: ctx,
            r:   r,
        }
    }
    
    func (cr *ContextReader) Read(p []byte) (int, error) {
        if err := cr.ctx.Err(); err != nil {
            return 0, err
        }
        return cr.r.Read(p)
    }
No goroutines or mutexes or whatever else required.

Extending to a ReadCloser is a simple exercise left to the, er, reader.

orf01/21/2025

A mutex in a hot Read (or any IO) path isn’t efficient.

show 1 reply