Repository: go
Issue: proposal: context: add context.Reader/context.Writer
Labels: Proposal,LibraryProposal

### Proposal Details

The stdlib has a fair amount of readers/writers that block until more content is available. For example:

- `io.Pipe()`
- `os.Pipe()`
- `net.TCPConn`

It can be tricky to handle context cancellation promptly when a goroutine is waiting for a blocking read/write.

## Existing workarounds

Let's look at some workarounds to implement a version of `io.Copy` that accepts a context and closes the writer when it's done.

### Putting cleanup code in context.AfterFunc()

```golang
func CopyAndClose(ctx context.Context, r io.Reader, w io.WriteCloser) error {
	stop := context.AfterFunc(ctx, func() {
		_ = w.Close()
	})
	defer func() {
		if stop() {
			_ = w.Close()
		}
	}()
	buf := make([]byte, 1024)
	for {
		if err := context.Cause(ctx); err != nil {
			return err
		}

		n, err := r.Read(buf)
		if errors.Is(err, io.EOF) {
			return nil
		}

		if err := context.Cause(ctx); err != nil {
			return err
		}

		_, err = w.Write(buf[:n])
		if err != nil {
			return err
		}
	}
}
```

Problems:

- Doesn't return immediately if it's blocked on a read/write call
- Requires even more synchronization than in the example in order to guarantee the function will never attempt to call `w.Write` after `w` has been closed.

### Using additional goroutines and channels to unblock function return

```golang
func CopyAndClose(ctx context.Context, r io.Reader, w io.WriteCloser) error {
	eg := &errgroup.Group{}
	c := make(chan []byte)

	eg.Go(func() error {
		defer close(c)
		buf := make([]byte, 1024)
		buf2 := make([]byte, 1024)
		for {
			n, err := r.Read(buf)
			if errors.Is(err, io.EOF) {
				return nil
			}
			select {
			case <-ctx.Done():
				return context.Cause(ctx)
			case c <- buf[:n]:
			}
			buf, buf2 = buf2, buf
		}
	})
	eg.Go(func() error {
		defer w.Close()
		for {
			select {
			case <-ctx.Done():
				return context.Cause(ctx)
			case buf, ok := <-c:
				if !ok {
					return nil
				}
				_, err := w.Write(buf)
				if err != nil {
					return err
				}
			}
		}
	})

	done := make(chan error)
	go func() {
		done <- eg.Wait()
		close(done)
	}()

	select {
	case err := <-done:
		return err
	case <-ctx.Done():
		go func() {
			<-done
		}()
		return context.Cause(ctx)
	}
}
```

Problems:

- Doesn't guarantee that `w` has been closed when the function returns
- Leaves several dangling goroutines that won't finish until the blocking Read/Write calls finish

## Proposal

Add something like this to `context`:

```golang
type Reader interface {
	io.Reader
	ContextRead(ctx Context, p []byte) (n int, err error)
}

func Read(ctx Context, r io.Reader, p []byte) (n int, err error) {
	if r, ok := r.(Reader); ok {
		return r.ContextRead(ctx, p)
	}
	if err := Cause(ctx); err != nil {
		return 0, err
	}
	return r.Read(p)
}

type Writer interface {
	io.Writer
	ContextWrite(ctx Context, p []byte) (n int, err error)
}

func Write(ctx Context, w io.Writer, p []byte) (n int, err error) {
	if w, ok := w.(Writer); ok {
		return w.ContextWrite(ctx, p)
	}
	if err := Cause(ctx); err != nil {
		return 0, err
	}
	return w.Write(p)
}
```

This would allow every reader/writer to implement their own `ContextRead`/`ContextWrite` methods as possible/necessary in order to unblock their calls on context cancellation. Having it in the stdlib would be especially beneficial because many of the readers/writers that folks would want to use this with are in the stdlib.

With these proposed changes, `CopyAndClose` would look like this:

```golang
func CopyAndClose(ctx context.Context, r io.Reader, w io.WriteCloser) error {
	defer w.Close()

	buf := make([]byte, 1024)
	for {
		n, err := context.Read(ctx, r, buf)
		if errors.Is(err, io.EOF) {
			return nil
		} else if err != nil {
			return err
		}

		_, err = context.Write(ctx, w, buf[:n])
		if err != nil {
			return err
		}
	}
}
```