Search code examples
goweb-development-server

Trouble understanding ServeHTTP - how does this code work?


I am studying web development in Golang (Beginner) I came across some code I played around with and I'm not too sure why it works, I looked through the library source code and docs but I only have a vague idea it still isn't clicking. Note the code below:

package main

import (
    "fmt"
    "net/http"
)

type foo int

func (m foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Some text")
}

func main() {
    var bar foo
    http.ListenAndServe(":8080", bar)
}

From what I understand adding *ServeHTTP(w http.ResponseWriter, r http.Request) as a function method, implements the handler interface (if I'm saying that correctly) and now foo is of type handler as well. I also understand that http.ListenAndServe takes an input of type handler so that is where my variable bar comes into play. When I run the code and go to localhost:8080 on my browser I get "Some Text" appearing.

Question:

How does this exactly work? How is that ServeHTTP function being accessed?

I tried looking at the source code of the libraries but couldn't pinpoint exactly how ServeHTTP worked. I found these two pieces of code (not sure if this is applicable) that sort of gave me the idea it was implementing a function but need clarification:

// 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)
}

I have never seen a type declaration as the one above with HandlerFunc that has a function after the name of the type. I have also seen how methods are declared but not sure what is happening in the code above.


Solution

  • How does this exactly work? How is that ServeHTTP function being accessed?

    To answer this question we need to look at how http.ListenAndServe works:

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

    Here we create a Server with given address and handler and call the ListenAndServer method, so let's take a look there:

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

    This method just starts listening at the given address and calls the Server method with our freshly created listener, so let's follow the trail there:

    func (srv *Server) Serve(l net.Listener) error {
        defer l.Close()
        
        ...
        
        for {
            rw, e := l.Accept()
    
            ...
    
            c := srv.newConn(rw)
            c.setState(c.rwc, StateNew) // before Serve can return
            go c.serve(ctx)
        }
    }
    

    From the Serve method we can see that this is the point where we accept the new connection and start handling it, in it's own goroutine.

    // Serve a new connection.
    func (c *conn) serve(ctx context.Context) {
        ...
        for {
            w, err := c.readRequest(ctx)
            ...
            serverHandler{c.server}.ServeHTTP(w, w.req)
            ...
        }
    }
    

    Here we finally call the ServeHTTP method, but as we can see this is not yet our implementation of that function, but something from the standard library, so let's take a look what that serverHandler struct contains:

    // serverHandler delegates to either the server's Handler or
    // DefaultServeMux and also handles "OPTIONS *" requests.
    type serverHandler struct {
        srv *Server
    }
    
    func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
        handler := sh.srv.Handler
        if handler == nil {
            handler = DefaultServeMux
        }
        if req.RequestURI == "*" && req.Method == "OPTIONS" {
            handler = globalOptionsHandler{}
        }
        handler.ServeHTTP(rw, req)
    }
    

    So here it is finally: If we didn't provide any Handler the DefaultServeMux will be used. but since we provided our foo handler ServeHTTP from foo get's called.

    And that's it. All of this can be found @ server.go