Description
Issue
As Go matures as a language it is being adopted in areas beyond simple TCP/UDP servers and
command line software. Some people are working with non-standard socket types (AF_NETLINK,
AF_ROUTE 10565, AF_PACKET 15021, AF_CAN 16188, etc). Also, in the embedded area it
is very common to receive data through TTY devices or os pipes. All of these use cases
would benefit from as easy way to set timeouts without having to launch extra user-level
go routines or to cancel read/write operations without having to close the socket.
The current idiomatic way to deal with a timeout read on a generic file descriptor is as
follows:
fd, _ := syscall.Open(<path>, syscall.O_CLOEXEC, 0644)
buf := make([]byte, 1024)
success := make(chan bool)
go func() {
_, err := syscall.Read(fd, buf)
if err == nil {
success <- true
}
}()
select {
case <-success:
fmt.Println(string(buf))
case <-time.After(5 * time.Second)
syscall.Close(fd)
fmt.Println("timeout")
}
This has a few issues:
- You need to close the file descriptor, this is often not what you want to do
- The write to the success channel is sort of racy, it can be worked around with some
clever channel work - Since the read is a direct syscall, it not only blocks a go-routine, it blocks on
os thread, and for embedded work with constrained resources this is something that
should be avoided as much as possible
Proposal
The Go runtime has a single epoll/kqueue/iocp thread setup to deal with exactly these
issues for standard TCP/UDP/unix sockets. This runtime poller should be exposed for
generic file descriptors with a new package called "os/poll". The package would would
allow wrapping a file descriptor with a "net.Conn"-like interface.
The proposed API looks like this:
type Descriptor struct {
// contains filtered or unexported fields
}
func Open(fd uintptr) (*Descriptor, error)
func (d *Descriptor) Close() error
func (d *Descriptor) Read(p []byte) (n int, err error)
func (d *Descriptor) Write(b []byte) (n int, err error)
func (d *Descriptor) SetDeadline(t time.Time) error
func (d *Descriptor) SetReadDeadline(t time.Time) error
func (d *Descriptor) SetWriteDeadline(t time.Time) error
func (d *Descriptor) Wait() error
func (d *Descriptor) WaitRead() error
func (d *Descriptor) WaitWrite() error
func (d *Descriptor) Cancel() error
This would provide a simple but flexible way to work with generic pollable file
descriptors. The example given above would then look like this:
fd, _ := syscall.Open(<path>, syscall.O_CLOEXEC, 0644)
buf := make([]byte, 1024)
desc, _ := poll.Open(fd)
desc.SetReadDeadline(time.Now().add(5 * time.Second)
_, err := desc.Read(buf)
if err != nil {
fmt.Println("timeout")
} else {
fmt.Println(string(buf)
}