RESP parser with bufio.Reader
bufio.Reader wraps net.Conn and gives us byte-level and line-level reads with internal buffering. Exactly the primitives a length-prefixed protocol needs.
Before the parser, dissect one frame by hand. The bytes a client sends for PING are 1\r\n$4\r\nPING\r\n. The first byte announces an array, the 1 says it has one element, and \r\n ends that header line. Next, $ announces a bulk string, 4 says the payload is four bytes, \r\n ends the header, then come the four payload bytes PING and a closing \r\n. Five type bytes cover all of RESP: + simple string, - error, : integer, $ bulk string, * array. Every value is a type byte, a header line ended by \r\n, and for bulk strings a length-prefixed payload. That is exactly the switch the parser below performs.
func ParseRESP(r *bufio.Reader) (any, error) {
typeByte, err := r.ReadByte()
if err != nil { return nil, err }
line, err := readLine(r)
if err != nil { return nil, err }
switch typeByte {
case '+': return line, nil
case '-': return errors.New(line), nil
case ':': return strconv.Atoi(line)
case '$':
n, _ := strconv.Atoi(line)
if n == -1 { return nil, nil }
buf := make([]byte, n+2)
io.ReadFull(r, buf)
return string(buf[:n]), nil
case '*':
count, _ := strconv.Atoi(line)
items := make([]any, count)
for i := range items { items[i], _ = ParseRESP(r) }
return items, nil
}
return nil, fmt.Errorf("unknown type byte %q", typeByte)
}The whole RESP parser in ~40 lines. Recursive for arrays. Returns any for heterogeneous values (string, int, []any, nil, error).
Go does not have native sum types. We use any (formerly interface{}) for the heterogeneous return. Callers type-switch on it. For a production parser you would define a typed RESPValue struct with a discriminator. The exercises walk through that refactor.
Quiz: Quiz
Loading practice…
AI prompt: Try it: hand-write a frame
Loading practice…