Context-aware, cancelable I/O for Go.
ctxio wraps files, network connections, pipes, TTYs, etc. so that reads and
writes can be canceled via a context.Context — without consuming data from the
underlying file descriptor. This is useful when you need to interrupt a blocking
Read or Write and then continue using the same connection or file
afterwards.
The standard library does not support canceling a blocking read without closing
the file descriptor. ctxio solves this by using OS-level I/O multiplexing
primitives to wait for data before issuing the actual read or write:
| Platform | Mechanism |
|---|---|
| Linux | epoll, select, deadlines |
| macOS, BSD | kqueue, select, deadline |
| Windows | Overlapped I/O, WaitForMultipleObjects, deadlines, CancelSynchronousIo |
| Generic Unix | select , deadlines |
| Others | deadlines |
When the context is canceled, the wait is interrupted and ErrCanceled is
returned. No data is lost because the actual read(2) / write(2) never
happened. Note that the io_uring mechanism for Linux was conciously not
implemented because it is disabled by default or compiled out of the kernel on
some distributions due to security concerns while the epoll backend achieves
the same results reliably.
Use a suitable Wrap* for your IO object and use either the context-aware
ReadContext/WriteContext methods or the regular io.ReadWriter interface
together with Cancel(), CancelReads(), CancelWrites(), Reset(),
ResetReader() and ResetWriter().
rawTTY, _ := os.Open("/dev/tty")
tty, _ := ctxio.WrapFile(rawTTY)
defer tty.Close()
buf := make([]byte, 256)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// cancel with a context
n, err := tty.ReadContext(ctx, buf)
if errors.Is(err, ctxio.ErrCanceled) {
fmt.Println("read timed out, but the file is still usable")
}
tty.Reset()
// or cancel explicitly
go func() {
time.Sleep(1 * time.Second)
tty.Cancel()
} ()
_, err = tty.Read(buf)There are two ways to cancel an operation:
- Context-based: Use
ReadContext/WriteContextwith a cancelable context. - Explicit: Call
Cancel()directly. This is useful when working with APIs that expect a plainio.Reader/io.Writer— theReadandWritemethods onContextIOcan be interrupted by callingCancel(),CancelReads()orCancelWrites()from another goroutine.
When Cancel() (or CancelReads() / CancelWrites()) is called, it affects
subsequent operations differently depending on which method you use:
Read/WritereturnErrCanceledimmediately until the correspondingReset()(orResetReader()/ResetWriter()) is called.ReadContext/WriteContextwith a fresh context automatically clear the canceled state before starting, so a fresh context is sufficient to resume I/O without an explicitReset()call.
cio, _ := ctxio.WrapFile(file)
cio.Cancel()
cio.Read(buf) // returns ErrCanceled (sticky)
// ReadContext auto-clears the canceled state and proceeds normally:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cio.ReadContext(ctx, buf) // proceeds normally
// To resume plain Read/Write after Cancel, call Reset explicitly:
cio.Cancel()
cio.Reset()
cio.Read(buf) // now works againConnect copies data between two ContextIO values in both directions and
respects context cancelation and in case an error occurs, it returns this error
instead of subsequent ones that result from the broken connection.
cioA, _ := ctxio.WrapFile(fileA)
cioB, _ := ctxio.WrapFile(fileB)
err := ctxio.Connect(ctx, cioA, cioB)ConnectAndClose does the same for arbitrary io.ReadWriteClosers. It achieves
this by closing both sides when an error occurs or the context is cancelled.
err := ctxio.ConnectAndClose(ctx, connA, connB)For objects that don't support the standard mechanisms (e.g., pipes, streams
from external libraries), the closable backend with WrapClosableReader,
WrapClosableWriter, or WrapClosableReadWriter, which provides the same API
with some caveats:
- Cancellation works by closing the underlying object, making it permanently unusable after cancellation.
- The object cannot be reused after
Cancel()is called, even after callingReset()—Reset()only clears the cancellation flag. - This backend is best used for one-shot operations or when the object is expected to be closed anyway.