Unix domain sockets offer efficient, secure, bidirectional communication between processes on a Unix/Linux machine. While channels are great for communicating across the goroutines of an application, and HTTP is ubiquitous, when communicating between Go applications (interprocess communication) running on the same machine channels are no help, and Unix socket communication is simpler, more efficient, and more secure than HTTP or other internet protocol communication.

Go’s net is the only package you need to start communicating:

Package net provides a portable interface for network I/O, including…Unix domain sockets.

Although the package provides access to low-level networking primitives, most clients will need only the basic interface provided by the Dial, Listen, and Accept functions and the associated Conn and Listener interfaces.

Unfortunately those functions and interfaces are sparsely documented (with regard to Unix domain sockets in particular), and there’s no official Go Blog post on how to get started with Unix socket communication. There seems to be a dearth of good introductions on StackOverflow and Go blogs. Most articles on Unix sockets demonstrate C implementations; here I’ll focus on how to get started in Go.

First, a Unix socket is represented as a special file. Your server listens on the file, accepts connections, and reads data from those connections. Your client dials the file to create a connection and then writes data to the connection.

You might think you need to create this special file using the os package. There you’ll find a FileMode constant, ModeSocket, that may call out to you. But this is no help: An undocumented feature of the Listen function is that it will – it must – create the file for you, and it will exit with a potentially misleading error (“bind: address already in use”) if the file already exists. Therefore your first, counterintuitive step in creating a server is to delete the file you wish to listen to, and then start listening to it:

    os.Remove(cfg.SocketFile)
    listener, err := net.Listen("unix", cfg.SocketFile)
    if err != nil {
        log.Fatalf("Unable to listen on socket file %s: %s", cfg.SocketFile, err)
    }
    defer listener.Close()

For a server to operate indefinitely and handle multiple simultaneous connections, you’ll want an infinite loop in which you accept a connection on the listener, and start a new goroutine to handle it:

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatalf("Error on accept: %s", err)
        }
        go message.HandleConn(conn)
    }

Your handler should create a buffer of whatever size you like, and read into the buffer in an infinite loop that will stop when Read errors. Read is barely documented in the net package. Strangely, you must know to look to the io package documentation for the Reader interface:

    type Reader interface {
            Read(p []byte) (n int, err error)
    }

…to learn important information about how Read behaves in other packages of the standard library:

Reader is the interface that wraps the basic Read method.

Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch space during the call. If some data is available but not len(p) bytes, Read conventionally returns what is available instead of waiting for more.

When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, it returns the number of bytes read. It may return the (non-nil) error from the same call or return the error (and n == 0) from a subsequent call. An instance of this general case is that a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil. The next Read should return 0, EOF.

Callers should always process the n > 0 bytes returned before considering the error err. Doing so correctly handles I/O errors that happen after reading some bytes and also both of the allowed EOF behaviors.

Implementations of Read are discouraged from returning a zero byte count with a nil error, except when len(p) == 0. Callers should treat a return of 0 and nil as indicating that nothing happened; in particular it does not indicate EOF.

Implementations must not retain p:

As cryptically stated above, the buffer you pass to Read in the form of a byte slice must have a length greater than zero for anything to be read into it. This is consistent with passing a slice rather than a pointer to a slice, because any increase of the length of the slice inside Read would not be visible in the calling context without use a of a pointer. A relatively common bug in using Read is to pass it a buffer of length zero.

Another common bug is to ignore the admonishment above to process the bytes returned prior to handling errors. This contrasts with common error handling advice in the majority of programming contexts in Go, and is important to correct implementation of net-based communication in general.

Here is an example handler that addresses those concerns. It reads into a buffer with length greater than zero inside an infinite loop, breaking only on error. After each Read, the first count bytes of the buffer are consumed prior to error handling:

func HandleConn(c net.Conn) {
    received := make([]byte, 0)
    for {
        buf := make([]byte, 512)
        count, err := c.Read(buf)
        received = append(received, buf[:count]...)
        if err != nil {
            ProcessMessage(received)
            if err != io.EOF {
                log.Errorf("Error on read: %s", err)
            }
            break
        }        
    }
}

By this method, all data sent on a connection is understood by the server as being one message. The client should close the connection to signal the end of the message. Reading the closed connection will return error io.EOF, which is routine and does not need to be handled as an error. It simply signifies that the message is ended, often a cue to begin processing the message now that it is complete.

What goes in ProcessMessage is, of course, dependent on your application. Since a string is basically a read-only slice of bytes, it is little effort to communicate text data in this way. But byte slices are also common currency in the Go standard library, and any data can be encoded as a slice of bytes.

All that’s left now is to make a client. The client is simply a function that dials up the socket file to create a connection, defers closing the connection, and writes the message bytes to the connection. One does not have to worry about what size the message is, it can be arbitrarily large, the code does not change. Here is an example with logrus-style error logging:

type Sender struct {
    Logger     *log.Logger
    SocketFile string
}

func (s *Sender) SendMessage(message []byte) {
    c, err := net.Dial("unix", s.SocketFile)
    if err != nil {
        s.Logger.Errorf("Failed to dial: %s", err)
    }
    defer c.Close()
    count, err := c.Write(message)
    if err != nil {
        s.Logger.Errorf("Write error: %s", err)
    }
    s.Logger.Infof("Wrote %d bytes", count)
}

A next step could be to add responses to acknowledge receipts. For many applications, the above is all that is needed to start communicating between Go processes using Unix domain sockets.