Skip to content

Instantly share code, notes, and snippets.

@mvdan
Created November 12, 2018 11:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mvdan/88d56364a8442f4e3ed185368b32a113 to your computer and use it in GitHub Desktop.
Save mvdan/88d56364a8442f4e3ed185368b32a113 to your computer and use it in GitHub Desktop.

Proposal: io: add a buffered pipe

Summary

Add a way to easily create a buffered io.Pipe. Currently, one has to write their own from scratch. This leads to dozens of separate implementations in the wild, and likely many correctness issues and hidden bugs.

Description

io.Pipe is synchronous, as per https://golang.org/pkg/io/#Pipe:

Reads and Writes on the pipe are matched one to one except when multiple Reads are needed to consume a single Write. That is, each Write to the PipeWriter blocks until it has satisfied one or more Reads from the PipeReader that fully consume the written data. The data is copied directly from the Write to the corresponding Read (or Reads); there is no internal buffering.

This is fine for most use cases. However, a buffered pipe can still be useful or necessary in many situations. Below is a list of third-party implementations I was able to find after a five-minute search:

Note the first two, which live in the standard library itself. They're also interesting, because net's requires deadlines, and http's supports any underlying pipe buffer implementation.

The main point of the proposal is that there are dozens of well-used buffered pipe implementations out there. This is particularly worrying, because writing a correct one isn't easy. It's very likely that some of the implementations out there have subtle bugs, such as misusing sync.Cond or being racy when many reads and writes happen concurrently.

I'm raising this proposal precisely because I had written my own buggy buffered pipe, which made go test -race find data races about 5% of the time. I "fixed" that by rewriting my tests to work around the lack of buffering, and using io.Pipe instead: https://github.com/mvdan/sh/commit/5ffc4d94f4d4e57cbb02063768b67c1b337bf9cc

Granted, my code being incorrect doesn't mean everyone's code is too. But it does help illustrate the point; writing a buffered pipe seems easy, and the lack of an implementation in the standard library seems to encourage that people write their own, likely buggy implementation.

This has been discussed in golang-dev before, although no common implementation was found or proposed: https://groups.google.com/forum/#!topic/golang-dev/k0bSal8eDyE

There's also a proposal to make net.Pipe asynchronous in Go2; golang/go#24205. That proposal is somewhat related to this one, since net.Pipe currently uses io.Pipe under the hood. This could however be changed in Go2.

Proposed APIs

The simplest change would be to add something like:

// BufferedPipe creates an asynchronous in-memory pipe, internally buffering up
// to n bytes.
func BufferedPipe(size int) (*PipeReader, *PipeWriter)

Ideally PipeReader and PipeWriter would have been interface types from the start. Within Go1, we can work around that by having the PipeWriter struct simply embed the actual pipe implementation, be it synchronous or asynchronous.

Another possible path in Go1 is to go the http2 route, allowing any underlying pipe buffer implementation. I personally think this would be a bit too niche for the io package, but perhaps @bradfitz has opinions on that.

If the net package needs read and write deadlines, they could always be added to the reader and writer returned by the constructors. This should be a backwards compatible change, and could be discussed as a separate proposal.

Finally, if we were to consider this as a Go2 change, the cleaned up version of my first proposed change would be:

type PipeReader interface {
	io.Reader
	io.Closer
	CloseWithError(err error) error
}

type PipeWriter interface {
	io.Writer
	io.Closer
	CloseWithError(err error) error
}

func Pipe() (PipeReader, PipeWriter)
func BufferedPipe(size int) (PipeReader, PipeWriter)

We could even go one step further and mirror make(chan T, size), only having func Pipe(size int) where Pipe(0) would return a synchronous/unbuffered pipe.

The net package could either mirror these proposed Go2 changes, or break with the connection to the io package entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment