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.
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
No goroutines or mutexes or whatever else required.Extending to a ReadCloser is a simple exercise left to the, er, reader.