Concurrent Servers: Part 8 - Go
This is part 8 in a series of posts on writing concurrent network servers. In this part, we'll switch to Go and see how it tackles the challenges described earlier in the series.
All posts in the series:
- Part 1 - Introduction
- Part 2 - Threads
- Part 3 - Event-driven
- Part 4 - libuv
- Part 5 - Redis case study
- Part 6 - Callbacks, Promises and async/await
- Part 7 - Rust
- Part 8 - Go (this part)
This post assumes a basic familiarity with the Go programming language.
Sequential state machine server
As before, we'll start with a sequential server for the basic state machine protocol presented in part 1.
This is the main function:
func main() {
port := "9090"
if len(os.Args) >= 2 {
port = os.Args[1]
}
log.Println("Serving on port", port)
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatal("Error listening:", err)
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Error accepting connection: %v", err)
continue
}
log.Println("connection received from", conn.RemoteAddr())
if err := server.ServeSerialProtocol(conn); err != nil {
log.Printf("Error serving %v: %v", conn.RemoteAddr(), err)
} else {
log.Println("peer done", conn.RemoteAddr())
}
}
}
As in the previous parts, the server is "infinite"; it never stops serving new connections until it's explicitly killed.
This is the function implementing the protocol for a single client; it takes a net.Conn value that represents a socket with a client connected on the other end:
type processingState int
const (
waitForMsg processingState = iota
inMsg
)
// ServeSerialProtocol serves our serial protocol to a single TCP connection.
func ServeSerialProtocol(conn net.Conn) error {
defer conn.Close()
if _, err := conn.Write([]byte{'*'}); err != nil {
return err
}
var state processingState = waitForMsg
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
for _, b := range buf[:n] {
switch state {
case waitForMsg:
if b == '^' {
state = inMsg
}
case inMsg:
if b == '