Non-blocking semantics for Go io package: first-class signals for would-block and multi-shot.
Language: English | 简体中文 | Español | 日本語 | Français
iox is for non-blocking I/O stacks where “no progress right now” and “progress now, but the operation remains active” are normal control flow, not failures.
It introduces two semantic errors with explicit contracts:
ErrWouldBlock— no progress is possible now without waiting for readiness/completions. Return immediately; retry after your next polling.ErrMore— progress happened and the operation remains active; more events will follow. Process the current result and keep polling.
iox keeps standard io mental models intact:
- returned counts always mean “bytes transferred / progress made”
- returned errors drive control flow (
nil, semantic non-failure, or real failure) - helpers are compatible with
io.Reader,io.Writer, and optimize viaio.WriterTo/io.ReaderFrom
For operations that adopt iox semantics:
| Return error | Meaning | What the caller must do next |
|---|---|---|
nil |
completed successfully for this call / transfer | continue your state machine |
ErrWouldBlock |
no progress possible now | stop attempting; wait for readiness/completion; retry |
ErrMore |
progress happened; more completions will follow | process now; keep the operation active; continue polling |
| other error | failure | handle/log/close/backoff as appropriate |
Notes:
iox.Copymay return(written > 0, ErrWouldBlock)or(written > 0, ErrMore)to report partial progress before stalling or before delivering a multi-shot continuation.(0, nil)reads are treated as “stop copying now” and return(written, nil)to avoid hidden spinning inside helpers.
The Go io.Reader contract allows Read to return (0, nil) to mean “no progress”, not end-of-stream.
Well-behaved Readers should avoid (0, nil) except when len(p) == 0.
iox.Copy intentionally treats a (0, nil) read as “stop copying now” and returns (written, nil).
This avoids hidden spinning inside a helper in non-blocking/event-loop code.
If you need strict forward-progress detection across repeated (0, nil), implement that policy at your call site.
When copying to a non-blocking destination, dst.Write may return a semantic error (ErrWouldBlock or ErrMore) with a partial write (nw < nr). In this case, bytes have been read from src but not fully written to dst.
To prevent data loss, iox.Copy attempts to roll back the source pointer:
- If
srcimplementsio.Seeker, Copy callsSeek(nw-nr, io.SeekCurrent)to rewind the unwritten bytes. - If
srcdoes not implementio.Seeker, Copy returnsErrNoSeekerto signal that unwritten bytes are unrecoverable.
Recommendations:
- Use seekable sources (e.g.,
*os.File,*bytes.Reader) when copying to non-blocking destinations. - For non-seekable sources (e.g., network sockets), use
CopyPolicywithPolicyRetryfor write-side semantic errors. This ensures all read bytes are written before returning, avoiding the need for rollback.
Install with go get:
go get code.hybscloud.com/ioxtype reader struct{ step int }
func (r *reader) Read(p []byte) (int, error) {
switch r.step {
case 0:
r.step++
return copy(p, "hello"), iox.ErrMore
case 1:
r.step++
return copy(p, "world"), nil
case 2:
r.step++
return 0, iox.ErrWouldBlock
case 3:
r.step++
return copy(p, "iox"), nil
default:
return 0, io.EOF
}
}
func main() {
src := &reader{}
var dst bytes.Buffer
n, err := iox.Copy(&dst, src)
fmt.Printf("n=%d err=%v buf=%q\n", n, err, dst.String()) // n=5 err=io: expect more buf="hello"
_, _ = iox.CopyN(io.Discard, &dst, 5) // consume "hello"
n, err = iox.Copy(&dst, src)
fmt.Printf("n=%d err=%v buf=%q\n", n, err, dst.String()) // n=5 err=io: would block buf="world"
_, _ = iox.CopyN(io.Discard, &dst, 5) // consume "world"
n, err = iox.Copy(&dst, src)
fmt.Printf("n=%d err=%v buf=%q\n", n, err, dst.String()) // n=3 err=<nil> buf="iox"
}-
Errors
ErrWouldBlock,ErrMore,ErrNoSeeker
-
Copy
Copy(dst Writer, src Reader) (int64, error)CopyBuffer(dst Writer, src Reader, buf []byte) (int64, error)CopyN(dst Writer, src Reader, n int64) (int64, error)CopyNBuffer(dst Writer, src Reader, n int64, buf []byte) (int64, error)
-
Tee
TeeReader(r Reader, w Writer) ReaderTeeWriter(primary, tee Writer) Writer
-
Adapters
AsWriterTo(r Reader) Reader(addsio.WriterToviaiox.Copy)AsReaderFrom(w Writer) Writer(addsio.ReaderFromviaiox.Copy)
-
Semantics
IsNonFailure(err error) boolIsWouldBlock(err error) boolIsMore(err error) boolIsProgress(err error) bool
-
Backoff
Backoff— adaptive back-off for external I/O waitingDefaultBackoffBase(500µs),DefaultBackoffMax(100ms)
When ErrWouldBlock signals that no progress is possible, the caller must wait before retrying. iox.Backoff provides an adaptive back-off strategy for this waiting.
Three-Tier Progress Model:
| Tier | Mechanism | Use Case |
|---|---|---|
| Strike | System call | Direct kernel hit |
| Spin | Hardware yield (spin) |
Local atomic synchronization |
| Adapt | Software backoff (iox.Backoff) |
External I/O readiness |
Zero-value ready to use:
var b iox.Backoff // uses DefaultBackoffBase (500µs) and DefaultBackoffMax (100ms)
for {
n, err := conn.Read(buf)
if err == iox.ErrWouldBlock {
b.Wait() // adaptive sleep with jitter
continue
}
if err != nil {
return err
}
process(buf[:n])
b.Reset() // reset on successful progress
}Algorithm: Block-based linear scaling with ±12.5% jitter to prevent thundering herds.
- Block 1: 1 sleep of
base - Block 2: 2 sleeps of
2×base - Block n: n sleeps of
min(n×base, max)
Methods:
Wait()— sleep for the current duration, then advanceReset()— restore to block 1SetBase(d)/SetMax(d)— configure timing
TeeReaderreturnsnas the number of bytes read fromr(source progress), even if the side write fails/is short.TeeWriterreturnsnas the number of bytes accepted byprimary(primary progress), even if the tee write fails/is short.- When
n > 0, a tee adapter may return(n, err)whereerrcomes from the side/tee (includingErrWouldBlock/ErrMore). Processp[:n]first. - For best interoperability with policy-driven helpers, return
ErrWouldBlock/ErrMoreas-is (avoid wrapping).
Some helpers accept an optional SemanticPolicy to decide what to do when they encounter ErrWouldBlock or ErrMore
(e.g., return immediately vs yield and retry).
The default is nil, which means non-blocking behavior is preserved: the helper returns ErrWouldBlock / ErrMore
to the caller and does not wait or retry on its own.
iox.Copy uses standard "io" fast paths when available:
- if
srcimplementsio.WriterTo,iox.CopycallsWriteTo - else if
dstimplementsio.ReaderFrom,iox.CopycallsReadFrom - else it uses a fixed-size stack buffer (
32KiB) and a read/write loop
To preserve ErrWouldBlock / ErrMore across fast paths, ensure your WriteTo / ReadFrom implementations return those errors when appropriate.
If you have a plain io.Reader/io.Writer but want the fast-path interfaces to exist and preserve semantics, wrap with:
iox.AsWriterTo(r)to add aWriteToimplemented viaiox.Copyiox.AsReaderFrom(w)to add aReadFromimplemented viaiox.Copy
MIT — see LICENSE.
©2025 Hayabusa Cloud Co., Ltd.