Unix domain sockets with Go

Let's say you have an application with two backend services. There is some message that the emitting service needs to transmit to the receiving service. Let's also assume that the receiving service is not trivially startable and needs to be a long-running server-like. For example, this could be the case if the receiving service has to respond very quickly while the startup takes some time.

In today's age, a lot of people would reach for some kind of message broker, like RabbitMQ or simpler DIY-implementations inside a database table, or Redis, or even proprietary cloud services. But for a lot of use cases, there is a simpler and more performant way that many people are not aware of: Unix domain sockets. Let's look at a simple example in Go:

Golang
package main

import (
	"fmt"
	"net"
	"os"
)

func handleMessage(conn net.Conn) {
	defer conn.Close()

	buf := make([]byte, 1024)

	_, err := conn.Read(buf)
	if err != nil {
		fmt.Printf("Error while trying to read message: %s\n", err.Error())
		os.Exit(1)
	}
	fmt.Printf("Received message: %s\n", string(buf))
}

func main() {
	tmpfs, ok := os.LookupEnv("XDG_RUNTIME_DIR")
	if !ok || tmpfs == "" {
		fmt.Println("Failed to lookup XDG_RUNTIME_DIR")
		os.Exit(1)
	}
	path := fmt.Sprintf("%s/worker", tmpfs)

	err := os.Remove(path)
	if err != nil && !os.IsNotExist(err) {
		fmt.Printf("Failed to remove path %s: %s", path, err.Error())
		os.Exit(1)
	}

	l, err := net.Listen("unix", path)
	if err != nil {
		fmt.Printf("Failed to open path %s: %s\n", path, err.Error())
		os.Exit(1)
	}

	fmt.Println("Listening...")
	for {
		conn, err := l.Accept()
		if err != nil {
			fmt.Printf("Failed to accept conn: %s\n", err.Error())
		    os.Exit(1)
		}

		go handleMessage(conn)
	}
}
			

This is the code of the receiving service, i.e. the server-like service. We need to first make sure the domain socket is created, this is done by net.Listen, when providing a file system path. We are creating the socket at the XDG_RUNTIME_DIR path. XDG_RUNTIME_DIR is an environment variable set by pam_systemd. However, there are some cases, where XDG_RUNTIME_DIR could be be unset. Within this simple demonstration program we are simply printing ut every message we receive. Now consider this client code:

Golang
package main

import (
	"fmt"
	"net"
	"os"
)

func main() {
	fmt.Println("Starting client...")
	tmpfs, ok := os.LookupEnv("XDG_RUNTIME_DIR")
	if !ok || tmpfs == "" {
		fmt.Println("Failed to lookup XDG_RUNTIME_DIR")
		os.Exit(1)
	}

	path := fmt.Sprintf("%s/worker", tmpfs)
	c, err := net.Dial("unix", path)
	if err != nil {
		fmt.Printf("Failed to dial socket %s: %s\n", path, err.Error())
		os.Exit(1)
	}

	fmt.Println("Sending message...")
	_, err = c.Write([]byte("some_message"))
	if err != nil {
		fmt.Println("Failed to write to domain socket")
		os.Exit(1)
	}

	fmt.Println("Client done")
}
			

As you can see, the actual code to connect both services is very simple and small.