Go: More UNIX than UNIX

posted Apr 29, 2014
in Programming, Golang

Go comes in part from Rob Pike and Ken Thompson, both influential in early UNIX. Both Rob Pike and Ken Thompson also were influential in working on Plan 9, a followup to UNIX.

UNIX's ideal is that "everything is a file". In Go terminology, this is a declaration that everything should be accessible via a uniform interface, which the OS specially privileges. One of Plan 9's core reasons for existing is that UNIX didn't take this anywhere near as far as it could be taken, and it goes much further in making everything accessible as a file in a directory structure.

I'm skeptical of both of these approaches. Everything isn't a "file".

There's numerous "files" that require ioctls to correctly manipulate, which are arbitrary extensions outside of the file interface. On the flip side, there are all kinds of "files" that can't be seeked, such as sockets, or files that can't be closed, like UDP streams. Pretty much every element of the file interface is one that doesn't apply to some "file", somewhere.

The Procrustean approach to software engineering tends to have the same results as Procrustes himself did, gravely or even fatally wounding the code in question.


Correctly, Go does not ship with a "file" interface and force everything into it, regardless of how appropriate it is... instead, it ships with a number of interfaces that describe the elements of what a file may do, and lets you easily compose them as appropriate. The io package ships with "here's how to read out a stream of bytes" (Reader), "here's how to write a stream of bytes" (Writer), and "here's how to close something" (Closer, and note how I say "something" because it may not be a stream). These have wide applicability to all sorts of things.

In the net package, we get the Conn interface, which implements Reader, Writer, and Closer, then implements a whole bunch of other things that make sense for connections but don't make sense for all the UNIX-conceived "files". And so on for the other cases of file-like things that aren't files... they'll implement the interfaces that make sense, and leave off the interfaces that don't. Real disk files have a concrete type that implements Reader, Writer, and Closer, and it adds file-specific things without forcing them on any other data type. While I don't think Go ships with a Seeker interface, you can implement your own as easily as simply declaring it, and a *os.File will automatically be an implementation of your new interface.

type Seeker interface {
    Seek(int64, int) (int64, error)
}

type Syncer interface {
    Sync() error
}

Done. Heck, I threw in a Syncer interface just for fun, because that's how easy it is.

This has practical consequences. godeb is a package for conveniently taking a go release and packing it into a Debian package, so the Debian package manager can manage it properly. It worked by scraping the Go downloads list and parsing what files were available to you. However, the 1.3 beta lives somewhere else, so this code couldn't see it. I was able to easily adapt the godeb code to take arbitrary tarballs from the disk, because the original godeb implementation used an io.Reader to read the tarball. As it happens, it read it straight off the network, in my case it read it off the disk, but the code as originally written didn't care. This made it an easy modification that required rewriting just a smidge of code, instead of a significant rewrite.

One of the things I dream of writing, or having written, is a go vet check (or similar) that flags code that claims more specificity in its type signature than it needed to. For instance, if a function takes a *os.File, but only ever calls read, this check should flag the function as taking only an io.Reader instead. If it takes an io.ReadWriteCloser but only ever calls Write, it should flag the code as taking a io.Writer. Ideally this should work with locally-defined interfaces too. This would make parametricity even easier to use in Go.

As I'm in a "cleanup & polish for release" phase on my current Go project at work, I find I'm doing this a lot, replacing concrete types with the correct interfaces, especially for New* constructors whose sole purpose is to return a particular implementation of an interface, but whose concrete implementation has no additional functionality worth exposing. Those should simply return the interface value, not the concrete value, if the concrete value adds no more value.

By giving us the idea of broadly-applicable interfaces without the Procrustean solution, Go is more UNIX than UNIX and more Plan 9 than Plan 9.

 

Site Links

 

RSS
All Posts

 

Blogroll