Skip to content

Instantly share code, notes, and snippets.

@jphsd
Last active October 13, 2024 16:46
Show Gist options
  • Save jphsd/5c503d22d826648e6ccd8414938fd709 to your computer and use it in GitHub Desktop.
Save jphsd/5c503d22d826648e6ccd8414938fd709 to your computer and use it in GitHub Desktop.
Interprocess channels in Go by using named pipes

How to use channels across different processes in Go

A couple of code samples to show how a named pipe can be used to extend Go's channel paradigm for use between different processes running on a system.

  • interprocess1.go details a single byte channel.
  • interprocess2.go details a channel that passes slices of bytes.

Note that opening a write channel will return two channels - one for writing to, and one which will be written to when the goroutine writing to the named pipe has completed. This last channel should be checked, prior to exiting a program, to avoid any loss of data.

The close() function should be called on the write channel once all data has been sent. This will cause any outstanding data and an EOF to be sent to the named pipe.

If you want to pass type instances over the named pipe then you will need to handle the encoding and decoding yourself.

Although intended for channel commnication between two Go processes, the other side of the named pipe can be any other process you like.

Note that named pipes can have multiple readers and writers although I don't recommend this as there are no guarantees about byte order and about who gets what.

// WriteChan opens the named pipe for writing and returns two channels,
// one for writing bytes to and one which will be written to when the writer goroutine completes.
func WriteChan(name string) (chan<- byte, <-chan byte, error) {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_APPEND, 0o200)
if err != nil {
return nil, nil, err
}
c := make(chan byte)
q := make(chan byte)
go writer(f, c, q)
return c, q, nil
}
func writer(f *os.File, c chan byte, q chan<- byte) {
defer f.Close()
for b := range c {
n, err := f.Write([]byte{b})
if n != 1 {
panic(err)
}
}
q <- 0
}
// ReadChan opens the named pipe for reading and returns a channel from which bytes can be read.
func ReadChan(name string) (<-chan byte, error) {
f, err := os.OpenFile(name, os.O_RDONLY, 0o400)
if err != nil {
return nil, err
}
c := make(chan byte)
go reader(f, c)
return c, nil
}
func reader(f *os.File, c chan byte) {
defer f.Close()
buf := []byte{0}
for {
n, err := f.Read(buf)
if n == 0 && err == io.EOF {
close(c)
break
}
if err != nil {
panic(err)
}
c <- buf[0]
}
}
// WriteChan opens the named pipe for writing and returns two channels,
// one for writing byte slices to and one which will be written to when the writer goroutine completes.
func WriteChan(name string) (chan<- []byte, <-chan byte, error) {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_APPEND, 0o200)
if err != nil {
return nil, nil, err
}
c := make(chan []byte)
q := make(chan byte)
go writer(f, c, q)
return c, q, nil
}
func writer(f *os.File, c chan []byte, q chan<- byte) {
defer f.Close()
for b := range c {
_, err := f.Write(b)
if err != nil {
panic(err)
}
}
q <- 0
}
// ReadChan opens the named pipe for reading and returns a channel from which byte slices can be read.
func ReadChan(name string) (<-chan []byte, error) {
f, err := os.OpenFile(name, os.O_RDONLY, 0o400)
if err != nil {
return nil, err
}
c := make(chan []byte)
go reader(f, c)
return c, nil
}
func reader(f *os.File, c chan []byte) {
defer f.Close()
max := 1024 // attempt to read this many bytes
for {
buf := make([]byte, max)
n, err := f.Read(buf)
if n == 0 && err == io.EOF {
close(c)
break
}
if err != nil {
panic(err)
}
buf = buf[:n]
c <- buf
}
}
@jphsd
Copy link
Author

jphsd commented Oct 13, 2024

I could have used sync.WorkGroup instead of the quit channel for the writer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment