A golang fifo (also called a golang named pipe or fifo file) lets two programs—or two goroutines—stream bytes in one direction through a special file on disk. Queries such as golang mkfifo, mkfifo example, linux fifo example, linux mkfifo example, mkfifo command in linux, or go fifo usually map to either the shell mkfifo utility or syscall.Mkfifo in Go. This tutorial shows both ways to create the node, a complete reader and writer in one program, and how O_RDONLY versus O_RDWR changes EOF behavior. You should be comfortable with goroutines and basic file I/O before trying fifos.
Tested with Go 1.24 on Linux.
Create a fifo file: mkfifo command in Linux and syscall.Mkfifo
Linux mkfifo example (shell):
mkfifo /tmp/demo.fifoIn another terminal you can cat /tmp/demo.fifo in one window and echo hello > /tmp/demo.fifo in another—the shell example is the quickest fifo files sanity check.
Golang mkfifo uses syscall.Mkfifo (Linux-oriented; named pipes differ on Windows):
if err := syscall.Mkfifo("/tmp/demo.fifo", 0o600); err != nil {
panic(err)
}Use a path under /tmp for experiments and remove the node when you are finished.
Reader and writer (go fifo) with O_RDONLY
Opening a fifo for read usually blocks until a writer opens it for write (and the opposite is also true). The program below creates the fifo, starts a writer goroutine that opens write-only, then opens read-only in main and prints lines until EOF after the writer closes. It needs a real Linux fifo on disk, so save it as a .go file and run go run locally (not in the in-browser Run environment).
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"syscall"
"time"
)
const fifoPath = "/tmp/gocheckfifo"
func main() {
_ = os.Remove(fifoPath)
if err := syscall.Mkfifo(fifoPath, 0o600); err != nil {
panic(err)
}
defer os.Remove(fifoPath)
go func() {
time.Sleep(200 * time.Millisecond)
w, err := os.OpenFile(fifoPath, os.O_WRONLY, 0)
if err != nil {
panic(err)
}
defer w.Close()
for i := 0; i < 3; i++ {
if _, err := fmt.Fprintf(w, "line%d\n", i); err != nil {
panic(err)
}
time.Sleep(50 * time.Millisecond)
}
}()
r, err := os.OpenFile(fifoPath, os.O_RDONLY, 0)
if err != nil {
panic(err)
}
defer r.Close()
br := bufio.NewReader(r)
for {
line, err := br.ReadBytes('\n')
if err != nil {
if err == io.EOF {
if len(line) > 0 {
fmt.Println("reader:", strings.TrimSpace(string(line)))
}
fmt.Println("reader: EOF")
break
}
panic(err)
}
fmt.Println("reader:", strings.TrimSpace(string(line)))
}
}Example output from go run on Linux:
reader: line0
reader: line1
reader: line2
reader: EOFO_RDONLY vs O_RDWR on a fifo
With os.O_RDONLY, when the last writer closes, the reader drains any buffered bytes and then observes io.EOF, which is what most linux fifo example readers expect.
Opening the same fifo with os.O_RDWR is sometimes used so OpenFile does not block waiting for a peer, but EOF semantics change because the fifo can remain logically open; you often combine RDWR with explicit framing, context timeouts, or shutting down the reader when your protocol ends. Avoid os.Exit in libraries because it skips deferred cleanup.
Summary
Golang fifo and golang named pipe workflows come down to creating a fifo node (mkfifo or syscall.Mkfifo), pairing a writer (O_WRONLY) with a reader (O_RDONLY), and handling io.EOF when writers finish. Golang mkfifo in code creates the same kind of fifo file as the mkfifo command in linux on the filesystem. Pick O_RDWR only when you understand the blocking and EOF trade-offs. For broader concurrency patterns, keep goroutines and Go channels in mind alongside fifos.
References
- Package syscall: Mkfifo
- Package os: OpenFile
- Go spec: Files (FIFOs are OS files)
- Goroutines
- Go channels

