Go's `http.ListenAndServe`

Implementation Overview

The ListenAndServe function is part of Go’s standard library in the net/http package. Its primary purpose is to start an HTTP server that listens on a specified address and handles incoming requests.

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

This function creates a Server instance and calls its ListenAndServe method.

Detailed Implementation

The core logic is in the Server.ListenAndServe and Server.Serve methods:

func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(ln)
}

func (srv *Server) Serve(l net.Listener) error {
    // ... (initialization code omitted)

    for {
        rw, e := l.Accept()
        if e != nil {
            // ... (error handling omitted)
        }
        c := srv.newConn(rw)
        go c.serve(connCtx)
    }
}

Goroutine Usage

  1. Main Goroutine:

    • The ListenAndServe function runs in the calling goroutine (often the main goroutine).
    • It enters an infinite loop in Serve, continuously accepting new connections.
  2. Per-Connection Goroutines:

    • For each accepted connection, a new goroutine is spawned (go c.serve(connCtx)).
    • This allows concurrent handling of multiple client connections.
  3. No Separate Listener Goroutine:

    • Unlike some server implementations, ListenAndServe doesn’t create a dedicated goroutine for listening.
    • The listening and accepting of connections occur in the same goroutine that called ListenAndServe.

Waiting Behavior

  1. Blocking Nature:

    • ListenAndServe is a blocking call. It doesn’t return until the server is closed or encounters an unrecoverable error.
    • The main loop in Serve continuously waits for new connections via l.Accept().
  2. Efficient Waiting:

    • While waiting for connections, the goroutine is in a “sleep” state, not consuming CPU cycles.
    • Go’s runtime uses efficient I/O polling mechanisms (like epoll or kqueue) under the hood.
  3. Error Handling and Shutdown:

    • The loop breaks and the function returns if Accept() returns a non-temporary error or if the server is shutting down.

Resource Consumption

  1. CPU Usage:

    • Minimal when waiting for connections, as the goroutine is mostly idle.
  2. Memory Usage:

    • Low and constant for the main goroutine.
    • Additional memory is used for each client connection goroutine.
  3. File Descriptors:

    • One for the listening socket, plus one for each active client connection.

Scalability and Performance

  • This design scales well, handling many concurrent connections efficiently.
  • The single listening goroutine doesn’t become a bottleneck, as accepting connections is typically very fast.
  • The per-connection goroutine model allows for high concurrency in request handling.

Conclusion

Go’s http.ListenAndServe implementation provides an efficient and scalable approach to running an HTTP server:

  • It uses goroutines effectively for concurrent connection handling.
  • The main goroutine blocks efficiently, waiting for new connections without consuming significant resources.
  • This design allows for simple usage in Go programs while providing robust performance for handling HTTP requests.