I recently ran into a puzzling error while working with Go socket connections. When performing a read operation, incoming messages would periodically arrive incomplete or not at all. The fix turned out to be easy, but the problem wasn’t immediately obvious to me.
Almost every Go server implementation is built on top of the io.Reader interface. Go also provides us bufio.Reader, which implements buffering for an io.Reader
object. It’s common to run into the following pattern for reading bytes from a connection:
data, err := bufio.NewReader(conn).ReadBytes('\r')
if err != nil {
// Handle error
}
But we rarely need to read just one message from a socket connection. To continuously wait for new messages, we can enclose our code in a for loop and hand off processing to another goroutine. If we’re feeling clever, we can reduce memory usage and processing time by using a pointer to pass off our data:
for {
data, err := bufio.NewReader(conn).ReadBytes('\r')
if err != nil {
// Handle error
}
go processData(&data)
}
Everything looks good so far, but this code actually introduces a big problem that you might not notice during testing: we’re creating a new bufio.Reader
, and underlying buffer, on every iteration. Since the buffer is not persistent across iterations, any messages received before the new Reader
is created will be lost.
The proper way to write this code is to create a new Reader
outside of the for loop. You can then call its read methods like normal on each loop iteration:
reader := bufio.NewReader(conn)
for {
data, err := reader.ReadBytes('\r')
if err != nil {
// Handle error
}
go processData(&data)
}