Exploring go net/http: How Server and the Handler structs works.

Add to bookmarks

Mon Nov 16 2020


The net/http is easily one of the most used libraries in go. This article is the first in a multipart series that will be looking at how certain functionalities work in the HTTP package. In this article, we'll be taking a look at Servers in go, the parts that make them work and examining a few code snippets on how it works.

A quick Http Lesson.

From the moment you make a curl request to a server a few things happen in the background (after DNS resolving has taken place). To simplify it: - A TCP (or whatever protocol is being used) connection is open by the client to the server. - An HTTP request is sent by the client to the server. - The server processes the request and sends a response to the client - More requests are sent by the client (e.g CSS resources) on the same connection as long as the keep-alive parameters still allow it.

Now that we've gotten that out of the way, the go net/http.Server is responsible for accepting and responding to HTTP connections and we will be taking a brief look at how part of it works, as we can't cover the full workings of the server in this article.

The Server Struct

Taking a look at net/http/server.go file in the go source code, we can take a look at the server struct item (with the comments removed for brevity):

type Server struct {
    Addr              string
    Handler           Handler
    TLSConfig         *tls.Config
    ReadTimeout       time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout      time.Duration
    IdleTimeout       time.Duration
    MaxHeaderBytes    int
    TLSNextProto      map[string]func(*Server, *tls.Conn, Handler)
    ConnState         func(net.Conn, ConnState)
    ErrorLog          *log.Logger
    BaseContext       func(net.Listener) context.Context
    ConnContext       func(ctx context.Context, c net.Conn) context.Context
    inShutdown        atomicBool 
    disableKeepAlives int32      
    nextProtoOnce     sync.Once  
    nextProtoErr      error      
    mu                sync.Mutex
    listeners         map[*net.Listener]struct{}
    activeConn        map[*conn]struct{}
    doneChan          chan struct{}
    onShutdown        []func()

There are quite a lot of fields in the struct (some self-explanatory and some are unneeded for this article) so I will be explaining a few briefly before going further on some later in the article:

  • Addr: This is the address that the server listens on if there is no listener passed to any of the Serve methods (To be touched later in the article)
  • Handler: The handler is basically responsible for responding to each request. It'll be explained further in the article.
  • TLSConfig: It holds the TLS configuration used by the server when responding to client requests.
  • ReadTimeout: The read timeout is the allowed amount of time allowed to read the whole http request, including the body.
  • WriteTimeout: The write timeout is the maximum amount of time the server waits before a response is written, starting from the moment the request is read. Think of it as the amount of time your handler has to respond to the request.
  • IdleTimeout: It is the maximum amount of time the server waits for the next request before closing the connection. Note that this doesn't matter if keep-alive isn't enabled in the connection.
  • BaseContext: It specifies the function that returns the context used by this server.
  • ConnContext: It modifies the base context for each new connection accepted by the server.

Setting up a simple server

Below is the code to set up a basic HTTP server that listens on the port 9091 and responds with Hello World:

package main

import (

func main() {
    http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
        fmt.Fprint(writer, "Hello world")
    server := http.Server{
        Addr: ":9091",
    fmt.Println("Starting server")

    if err := server.ListenAndServe(); err != nil {

The server code above is split up into three parts:

  • The HandleFunc, which is responsible for setting up the handler.
  • The server config which sets up the address for the listener.
  • The actual running of the server with the ListenAndServe function.

Now, we'd be going through each of these and how they help get a go server up and running.


What is the handler? The handler is the struct responsible for responding to individual requests, for a struct to be considered a handler, it has to satisfy the interface:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)

E.g if we were to pass a handler directly to the server we created above, the code above could be rewritten to:


type customHandler struct {
    response string

func (c *customHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
    fmt.Fprint(writer, c.response)

func main() {
    server := http.Server{
        Addr:    ":9091",
        Handler: &customHandler{"Hello world from the custom handler"},
    fmt.Println("Starting server")

    if err := server.ListenAndServe(); err != nil {

The resulting server would look like: browser

So here we create a customHandler Handler and pass it on to the server to be used to handle responses. Which begs the question, how did our earlier code get the server to handle our request initially without explicitly passing a handler?


When there is no handler explicitly passed to the handler, the DefaultServerMux (which is a ServeMux) declared in the net/http is used. If we take a look at the /net/http/server.go at the following lines:

1810 // Serve a new connection.
1811 func (c *conn) serve(ctx context.Context) {
1946        serverHandler{c.server}.ServeHTTP(w, w.req)
1977 }

2873 func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
2874    handler := sh.srv.Handler
2875    if handler == nil {
2876        handler = DefaultServeMux
2877    }
2881    handler.ServeHTTP(rw, req)
2882 }

We can see where the DefaultServeMux is used (lines 2875 - 2877) once the serveHandler.ServeHttp is called in the connection serving method on line 1811.

So when the http.HandleFunc function is called. Here is what happens:

2491 func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
2492    if handler == nil {
2493        panic("http: nil handler")
2494    }
2495    mux.Handle(pattern, HandlerFunc(handler))
2496 }
2506 func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
2507    DefaultServeMux.HandleFunc(pattern, handler)
2508 }

*Note the existense of HandlerFunc for now


ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL. Source: godoc

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry // slice of entries sorted from longest to shortest.
    hosts bool       // whether any patterns contain hostnames

type muxEntry struct {
    h       Handler
    pattern string

The attributes of the ServeMux are quite straight forward:

  • mu is a mutex lock and prevents race conditions.
  • m holds a map of patterns to a muxEntry. A muxEntry is essentially a handler and pattern.
  • es is the sorted lists of muxEntries from longest to shortest (this makes resolving URLs a lot more accurate)

Taking a look at DefaultServerMux's explanation above, we see that every call to HandleFunc of the ServeMux calls .Handle on the mux while passing the pattern and a HandlerFunc:

2445 // Handle registers the handler for the given pattern.
2445 // If a handler already exists for pattern, Handle panics.
2446 func (mux *ServeMux) Handle(pattern string, handler Handler) {
2447    mux.mu.Lock()
2448    defer mux.mu.Unlock()
2450    if pattern == "" {
2451        panic("http: invalid pattern")
2452    }
2453    if handler == nil {
2454        panic("http: nil handler")
2455    }
2456    if _, exist := mux.m[pattern]; exist {
2457        panic("http: multiple registrations for " + pattern)
2458    }

2459    if mux.m == nil {
2460        mux.m = make(map[string]muxEntry)
2461    }
2462    e := muxEntry{h: handler, pattern: pattern}
2463    mux.m[pattern] = e
2464    if pattern[len(pattern)-1] == '/' {
2465        mux.es = appendSorted(mux.es, e)
2466    }
2468    if pattern[0] != '/' {
2469        mux.hosts = true
2470    }
2471 }

Pretty straightforward code to follow. The mux checks if the m map has been initialized (it initializes it if it hasn't). Then adds a muxEntry to the map for that pattern.

Remember the ServerMux is still a Handler, so it has to have a ServeHTTP(ResponseWriter, *Request) function. What does that look like? (comments added by me for explanation)

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            // if this is at least http 1.1 send a close connection header since keep-alive is enabled by default in http > 1.1
            w.Header().Set("Connection", "close")

    // .Handler picks the handler that matches the request the most, the next line runs the 
    // ServeHTTP method of the selected handler
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)

How the ServerMux decides which handler to use is a bit beyond the scope of this article (for brevity), all you need to know is the multiplexer picks the handler based on the closest path match. Handlers like gorilla mux take this to a whole new level, so you can always check that out.


Remember the HandlerFunc from the ServerMux.HandleFunc method?

2491 func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
2495    mux.Handle(pattern, HandlerFunc(handler))
2496 }

The HandlerFunc is an adapter is an adapter that converts a func(ResponseWriter, *Request) into a Handler. How?

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)

So when it is used on line 2495 (mux.Handle(pattern, HandlerFunc(handler))) above, it covers the second parameter of ServerMux.HandleFunc, which is a function that accepts a ResponseWriter and Request (func(ResponseWriter, *Request)) into a Handler

The Actual Server

The important part of the server that we will be looking at is the Server.Serve(net.Listener). This is the method responsible for accepting connections via the listener and responding to requests via the handler.

In our initial code sample. The example called the server.ListenAndServe function. A quick look at that function and we see that it calls the .Serve function (comments added by me for explanatory purposes):

func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed

    // check if the address is set
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    // create a listener for tcp connections on the specified address
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    // serve the "server"
    return srv.Serve(ln)

Now, as you can see, if you were calling the Serve function directly you would need to pass a listener, but the ListenAndServe already creates one for us based on the address passed to the server configuration.

Next, we're taking a look at the actual Serve function:

2945 func (srv *Server) Serve(l net.Listener) error {
2963    baseCtx := context.Background()
2964    if srv.BaseContext != nil {
2965        baseCtx = srv.BaseContext(origListener)
2966        if baseCtx == nil {
2967            panic("BaseContext returned a nil context")
2968        }
2969    }
2971    var tempDelay time.Duration // how long to sleep on accept failure
2973    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
2974    for {
2975        rw, err := l.Accept()
2976        if err != nil {
2977            select {
2978            case <-srv.getDoneChan():
2979                return ErrServerClosed
2980            default:
2981            }
2982            if ne, ok := err.(net.Error); ok && ne.Temporary() {
2983                if tempDelay == 0 {
2984                    tempDelay = 5 * time.Millisecond
2985                } else {
2986                    tempDelay *= 2
2987                }
2988                if max := 1 * time.Second; tempDelay > max {
2989                    tempDelay = max
2990                }
2991                srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
2992                time.Sleep(tempDelay)
2993                continue
2994            }
2995            return err
2996        }
2997        connCtx := ctx
2998        if cc := srv.ConnContext; cc != nil {
2999            connCtx = cc(connCtx, rw)
3000            if connCtx == nil {
3001                panic("ConnContext returned nil")
3002            }
3003        }
3004        tempDelay = 0
3005        c := srv.newConn(rw)
3006        c.setState(c.rwc, StateNew, runHooks) // before Serve can return
3007        go c.serve(connCtx)
3008    }
3009 }

Line 2963 - 2969 set up the base context that will be passed around through the duration of the connection. It checks if the BaseContext Attribute of the Server (in the server struct section) and uses that to whip up the base server context. If nil, a background (default) contexts is used. Afterwards, the server is added to the context as a context value for later.

The next interesting part is the infinite loop on line 2974. The first step of the infinite loop is to wait for a net.Conn ( a connection ) via the listener's Accept() function. If there is an error, it continues the loop, after waiting for the duration of tempDelay till the max time is reached before the error is returned. After that, from line 2997 the context is cloned and modified with the ConnContext attribute of the Server from earlier. Then, on line 3005 a new http.conn struct is generated from the net.Conn ( a network connection) received from the listener earlier (line 2975).

What is the http.conn? It is more or less the HTTP package's representation of the server-side of an HTTP connection. It holds an immutable Server struct, and the net.Conn struct it is derived from (Code below):

type conn struct {
    // server is the server on which the connection arrived.
    // Immutable; never nil.
    server *Server

    // cancelCtx cancels the connection-level context.
    cancelCtx context.CancelFunc

    // rwc is the underlying network connection.
    // This is never wrapped by other types and is the value given out
    // to CloseNotifier callers. It is usually of type *net.TCPConn or
    // *tls.Conn.
    rwc net.Conn

    // remoteAddr is rwc.RemoteAddr().String(). It is not populated synchronously
    // inside the Listener's Accept goroutine, as some implementations block.
    // It is populated immediately inside the (*conn).serve goroutine.
    // This is the value of a Handler's (*Request).RemoteAddr.
    remoteAddr string

    // tlsState is the TLS connection state when using TLS.
    // nil means not TLS.
    tlsState *tls.ConnectionState

    // werr is set to the first write error to rwc.
    // It is set via checkConnErrorWriter{w}, where bufw writes.
    werr error

    // r is bufr's read source. It's a wrapper around rwc that provides
    // io.LimitedReader-style limiting (while reading request headers)
    // and functionality to support CloseNotifier. See *connReader docs.
    r *connReader

    // bufr reads from r.
    bufr *bufio.Reader

    // bufw writes to checkConnErrorWriter{c}, which populates werr on error.
    bufw *bufio.Writer

    // lastMethod is the method of the most recent request
    // on this connection, if any.
    lastMethod string

    curReq atomic.Value // of *response (which has a Request in it)

    curState struct{ atomic uint64 } // packed (unixtime<<8|uint8(ConnState))

    // mu guards hijackedv
    mu sync.Mutex

    // hijackedv is whether this connection has been hijacked
    // by a Handler with the Hijacker interface.
    // It is guarded by mu.
    hijackedv bool

Lastly, the generated http.conn's serve function is called on a new goroutine to listen for requests on that connection:

1810 // Serve a new connection.
1811 func (c *conn) serve(ctx context.Context) {
1872    for {
1873        w, err := c.readRequest(ctx)
1874        if c.r.remain != c.server.initialReadLimitSize() {
1875            // If we read any bytes off the wire, we're active.
1876            c.setState(c.rwc, StateActive, runHooks)
1877        }
1878        if err != nil {
1879            const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"
1881            switch {
1882            case err == errTooLarge:
1883                // Their HTTP client may or may not be
1884                // able to read this if we're
1885                // responding to them and hanging up
1886                // while they're still writing their
1887                // request. Undefined behavior.
1888                const publicErr = "431 Request Header Fields Too Large"
1889                fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
1890                c.closeWriteAndWait()
1891                return
1893            case isUnsupportedTEError(err):
1894                // Respond as per RFC 7230 Section 3.3.1 which says,
1895                //      A server that receives a request message with a
1896                //      transfer coding it does not understand SHOULD
1897                //      respond with 501 (Unimplemented).
1898                code := StatusNotImplemented
1890                // We purposefully aren't echoing back the transfer-encoding's value,
1891                // so as to mitigate the risk of cross side scripting by an attacker.
1892                fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)
1893                return
1895            case isCommonNetReadError(err):
1896                return // don't reply
1898            default:
1899                if v, ok := err.(statusError); ok {
1900                    fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s: %s%s%d %s: %s", v.code, StatusText(v.code), v.text, errorHeaders, v.code, StatusText(v.code), v.text)
1901                    return
1902                }
1903                publicErr := "400 Bad Request"
1904                fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
1905                return
1906            }
1907        }
1939        // HTTP cannot have multiple simultaneous active requests.[*]
1940        // Until the server replies to this request, it can't read another,
1941        // so we might as well run the handler in this goroutine.
1942        // [*] Not strictly true: HTTP pipelining. We could let them all process
1943        // in parallel even if their responses need to be serialized.
1944        // But we're not going to implement HTTP pipelining because it
1945        // was never deployed in the wild and the answer is HTTP/2.
1946        serverHandler{c.server}.ServeHTTP(w, w.req)
1947        w.cancelCtx()
1948        if c.hijacked() {
1949            return
1950        }
1951        w.finishRequest()
1952        if !w.shouldReuseConnection() {
1953            if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
1954                c.closeWriteAndWait()
1955            }
1956            return
1957        }
1958        c.setState(c.rwc, StateIdle, runHooks)
1959        c.curReq.Store((*response)(nil))
1961        if !w.conn.server.doKeepAlives() {
1962            // We're in shutdown mode. We might've replied
1963            // to the user without "Connection: close" and
1964            // they might think they can send another
1965            // request, but such is life with HTTP/1.1.
1967            return
1968        }
1970        if d := c.server.idleTimeout(); d != 0 {
1971            c.rwc.SetReadDeadline(time.Now().Add(d))
1972            if _, err := c.bufr.Peek(4); err != nil {
1973                return
1974            }
1975        }
1976        c.rwc.SetReadDeadline(time.Time{})
1977    }
1978 }

There are quite a few things going on in this function (remember it happens in a separate goroutine), also, most of theTLS parts of this function have been left due to scope.

The first thing is the infinite for loop on line 1827 which blocks till it reads the next request from the connection on line 18273. Then error checks happen from line 1878 - 1907, it is important to notice how during the error checks, HTTP information is written back on the net.Conn (since the connection implements an io.Writer interface). For example, the errorHeaders const on line 1879 contains a header that sends a close connection signal to the client.

After that, on line 1846. A serverHandler struct is created with the Server struct, and the ServeHTTP method is called. What does that look like? (comments added for explanatory purposes):

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    // get the servers handler or use DefaultServeMux if nil
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    // call the ServeHTTP on the handler
    handler.ServeHTTP(rw, req)

Now, the remaining part of the conn.serve function is honestly for tidying up things so we won't be going into those.


Quite a lot was touched in this article, but we have been able to walk through the process of accepting and responding to connections and requests, and we have been able to see how the sample server we set up, worked under a few layers of the hood.

Hope this article helped, if it did, don't forget to share.