6

Introduction

Almost all programming languages use Hello World as an example of an introductory program, and some of them start with writing a Web server as a practical case. Each programming language has many libraries for writing Web servers, either as standard libraries or through third-party libraries. Go language is no exception. This article and subsequent articles will explore the various Web programming frameworks in the Go language, their basic usage, read their source code, and compare their advantages and disadvantages. Let's start with the Go language standard library net/http . The standard library net/http makes it very easy to write a Web server. Let's explore together how to use the net/http library to implement some common functions or modules. Understanding these will be very helpful for us to learn other libraries or frameworks.

Hello World

It is very simple to write a simple Web server net/http

package main

import (
  "fmt"
  "net/http"
)

func index(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, "Hello World")
}

func main() {
  http.HandleFunc("/", index)
  http.ListenAndServe(":8080", nil)
}

First, we call http.HandleFunc("/", index) register the path processing function, here the path / is set to index . The type of the processing function must be:

func (http.ResponseWriter, *http.Request)

Among them, *http.Request represents the HTTP request object, which contains all the requested information, such as URL, header, form content, and other requested content.

http.ResponseWriter is an interface type:

// net/http/server.go
type ResponseWriter interface {
  Header() Header
  Write([]byte) (int, error)
  WriteHeader(statusCode int)
}

It sends a response to the client, implements ResponseWriter interface type apparently achieved io.Writer interface. So in the processing function index , you can call fmt.Fprintln() to write response information ResponseWriter

Carefully read the source code of the HandleFunc() net/http

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  DefaultServeMux.HandleFunc(pattern, handler)
}

We found that it directly called the HandleFunc() method of an object DefaultServeMux DefaultServeMux is an instance of type ServeMux

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
}

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

The usage of providing default type instances like this is very common in various libraries of the Go language. ServeMux saves the correspondence between all registered paths and processing functions. ServeMux.HandleFunc() method is as follows:

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

Here, the processing function handler converted to HandlerFunc type, and then the ServeMux.Handle() method is called to register. Note that the HandlerFunc(handler) here is a type conversion, not a function call. The definition of the HandlerFunc

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  f(w, r)
}

HandlerFunc function type func(ResponseWriter, *Request) as the underlying type, and defines the method ServeHTTP HandlerFunc type. Yes, the Go language allows you to define methods for (based on) function types. Serve.Handle() method only accepts parameters Handler

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}

func (mux *ServeMux) Handle(pattern string, handler Handler) {
  if mux.m == nil {
    mux.m = make(map[string]muxEntry)
  }
  e := muxEntry{h: handler, pattern: pattern}
  if pattern[len(pattern)-1] == '/' {
    mux.es = appendSorted(mux.es, e)
  }
  mux.m[pattern] = e
}

Obviously HandlerFunc implements the interface Handler . HandlerFunc type is just for the convenience of registering function type processors. Of course, we can directly define a Handler interface, and then register an instance of that type:

type greeting string

func (g greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintln(w, g)
}

http.Handle("/greeting", greeting("Welcome, dj"))

We define a new type greeting based on the string type, and then define a method ServeHTTP() (implementing the interface Handler ) for it, and finally call the http.Handle() method to register the processor.

For convenience of distinction, we will HandleFunc() registered handler is called, through Handle() register referred processor. It is not difficult to see through the above source code analysis that they are essentially the same thing at the bottom.

After registering the processing logic, call http.ListenAndServe(":8080", nil) monitor port 8080 of the local computer and start processing the request. Let's look at the processing of the source code:

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

ListenAndServe creates an Server type 060ee2209c9385:

type Server struct {
  Addr string
  Handler Handler
  TLSConfig *tls.Config
  ReadTimeout time.Duration
  ReadHeaderTimeout time.Duration
  WriteTimeout time.Duration
  IdleTimeout time.Duration
}

Server structure has more fields. We can use these fields to adjust the parameters of the Web server. For example, the above ReadTimeout/ReadHeaderTimeout/WriteTimeout/IdleTimeout used to control the reading and writing and idle timeout. In this method, first call the net.Listen() listening port, and Server.Serve() method net.Listener as a parameter:

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

In the Server.Serve() method, an infinite for loop is used, and the Listener.Accept() method is continuously called to accept new connections, and a new goroutine is opened to handle the new connections:

func (srv *Server) Serve(l net.Listener) error {
  var tempDelay time.Duration // how long to sleep on accept failure
  for {
    rw, err := l.Accept()
    if err != nil {
      if ne, ok := err.(net.Error); ok && ne.Temporary() {
        if tempDelay == 0 {
          tempDelay = 5 * time.Millisecond
        } else {
          tempDelay *= 2
        }
        if max := 1 * time.Second; tempDelay > max {
          tempDelay = max
        }
        srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
        time.Sleep(tempDelay)
        continue
      }
      return err
    }
    tempDelay = 0
    c := srv.newConn(rw)
    go c.serve(connCtx)
  }
}

Here is a exponential backoff strategy . If the l.Accept() returns an error, we judge whether the error is temporary ( ne.Temporary() ). If it is a temporary error, Sleep will try again after a short period of time. Every time a temporary error occurs, Sleep will double, up to Sleep 1s. After obtaining the new connection, encapsulate it into a conn object ( srv.newConn(rw) ), create a goroutine to run its serve() method. The code that omits irrelevant logic is as follows:

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

serve() method is actually to constantly read the requests sent by the client, create a serverHandler object and call its ServeHTTP() method to process the request, and then do some cleanup work. serverHandler is just an intermediate auxiliary structure, the code is as follows:

type serverHandler struct {
  srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  handler := sh.srv.Handler
  if handler == nil {
    handler = DefaultServeMux
  }
  handler.ServeHTTP(rw, req)
}

Get Handler from the Server object. This Handler is the second parameter passed in when http.ListenAndServe() In Hello World sample code, we passed the nil . So here handler will take the default value DefaultServeMux . Call the DefaultServeMux.ServeHTTP() method to process the request:

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
  h, _ := mux.Handler(r)
  h.ServeHTTP(w, r)
}

mux.Handler(r) finds the processor through the requested path information, and then calls the ServeHTTP() method of the processor to process the request:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
  host := stripHostPort(r.Host)
  return mux.handler(host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
  h, pattern = mux.match(path)
  return
}

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
  v, ok := mux.m[path]
  if ok {
    return v.h, v.pattern
  }

  for _, e := range mux.es {
    if strings.HasPrefix(path, e.pattern) {
      return e.h, e.pattern
    }
  }
  return nil, ""
}

The above code omits a lot of irrelevant code. In the match method, it first checks whether the path exactly matches mux.m[path] . If it cannot match exactly, the following for loop will match the longest prefix of the path. long as / root path processing, all unmatched paths will eventually be handed over to / path processing . In order to ensure that the longest prefix takes precedence, the paths are sorted during registration. So mux.es is stored in 060ee2209c955e is a processing list sorted by path:

func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
  n := len(es)
  i := sort.Search(n, func(i int) bool {
    return len(es[i].pattern) < len(e.pattern)
  })
  if i == n {
    return append(es, e)
  }
  es = append(es, muxEntry{})
  copy(es[i+1:], es[i:])
  es[i] = e
  return es
}

Run, type in the URL localhost:8080 in the browser, you can see the web page displays Hello World . Type in the URL localhost:8080/greeting , and see the web page displays Welcome, dj .

Thought questions:
According to the logic of the longest prefix, if you type localhost:8080/greeting/a/b/c , it should match the /greeting path.
If you type localhost:8080/a/b/c , it should match the / path. Is that right? The answer is behind 😀.

Created ServeMux

Call http.HandleFunc()/http.Handle() are the processor / function registered to ServeMux default object DefaultServeMux on. There is a problem with using the default object: uncontrollable.

First, Server parameters all use default values, and second, third-party libraries may also use this default object to register some processing, which is easy to conflict. What's more serious is that if we call http.ListenAndServe() start the Web service without knowing it, then the processing logic of the third-party library registration can be accessed through the network, which poses a great security risk. Therefore, it is not recommended to use the default object except in the sample program.

We can use http.NewServeMux() create a new ServeMux object, and then create http.Server target custom parameters, with ServeMux object initialization Server of Handler field, and finally call Server.ListenAndServe() ways to open Web services:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.Handle("/greeting", greeting("Welcome to go web frameworks"))

  server := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  20 * time.Second,
    WriteTimeout: 20 * time.Second,
  }
  server.ListenAndServe()
}

This program has Hello World , we also set an additional read and write timeout.

In order to facilitate understanding, I have drawn two pictures. In fact, the whole process is not complicated to organize:

Middleware

Sometimes it is necessary to add some general logic to the request processing code, such as statistical processing time, recording logs, capturing downtime, and so on. If you add these logic to each request processing function, the code will quickly become unmaintainable, and adding new processing functions will also become very cumbersome. So there is a need for middleware.

Middleware is a bit like an aspect-oriented programming idea, but it is different from the Java language. In Java, general processing logic (also called aspects) can be inserted into the processing flow of normal logic through reflection, which is basically not done in the Go language.

In Go, middleware is implemented through function closures. Functions in Go language are the first type of values, which can be passed to other functions as parameters or returned from other functions as return values. We introduced the use and implementation of processors/functions earlier. Then you can use closures to encapsulate existing processing functions.

First, define a middleware type based on the function type func(http.Handler) http.Handler

type Middleware func(http.Handler) http.Handler

Next, let's write middleware. The simplest middleware is to output a log before and after the request:

func WithLogger(handler http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    logger.Printf("path:%s process start...\n", r.URL.Path)
    defer func() {
      logger.Printf("path:%s process end...\n", r.URL.Path)
    }()
    handler.ServeHTTP(w, r)
  })
}

The implementation is very simple, the original processor object is encapsulated through middleware, and then a new processing function is returned. In the new processing function, first output the log of the beginning of the processing, and then use the defer statement to output the log of the end of the processing after the function ends. ServeHTTP() method of the original processor object to execute the original processing logic.

Similarly, let's implement a time-consuming middleware for statistical processing:

func Metric(handler http.Handler) http.HandlerFunc {
  return func (w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    defer func() {
      logger.Printf("path:%s elapsed:%fs\n", r.URL.Path, time.Since(start).Seconds())
    }()
    time.Sleep(1 * time.Second)
    handler.ServeHTTP(w, r)
  }
}

Metric middleware encapsulates the original processor object, records the time before execution, and takes time to output after execution. To make it easier to see the results, I added a time.Sleep() call to the above code.

Finally, since the request processing logic is written by the function developer (not the library author), for the stability of the Web server, we need to capture the panic that may appear. PanicRecover middleware is as follows:

func PanicRecover(handler http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    defer func() {
      if err := recover(); err != nil {
        logger.Println(string(debug.Stack()))
      }
    }()

    handler.ServeHTTP(w, r)
  })
}

Call recover() function to capture panic, output stack information, in order to prevent the program from exiting abnormally. In fact, there is also recover() conn.serve() method, and the program generally does not exit abnormally. But custom middleware can add our own custom logic.

Now we can register the processing function like this:

mux.Handle("/", PanicRecover(WithLogger(Metric(http.HandlerFunc(index)))))
mux.Handle("/greeting", PanicRecover(WithLogger(Metric(greeting("welcome, dj")))))

This method is a bit cumbersome. We can write a helper function that accepts the original processor object and multiple middlewares that are variable. Apply these middleware to the processor object and return the new processor object:

func applyMiddlewares(handler http.Handler, middlewares ...Middleware) http.Handler {
  for i := len(middlewares)-1; i >= 0; i-- {
    handler = middlewares[i](handler)
  }

  return handler
}

Note that the application sequence is from right to left , that is, right combined with , the closer to the original processor, the later the execution.

Using the help function, registration can be simplified to:

middlewares := []Middleware{
  PanicRecover,
  WithLogger,
  Metric,
}
mux.Handle("/", applyMiddlewares(http.HandlerFunc(index), middlewares...))
mux.Handle("/greeting", applyMiddlewares(greeting("welcome, dj"), middlewares...))

The above registration processing logic needs to call the applyMiddlewares() function once, which is still slightly cumbersome. We can optimize this way, encapsulate our own ServeMux structure, and then define a method Use() to save the middleware, rewrite Handle/HandleFunc to http.HandlerFunc/http.Handler processor and then pass it to the underlying ServeMux.Handle() method:

type MyMux struct {
  *http.ServeMux
  middlewares []Middleware
}

func NewMyMux() *MyMux {
  return &MyMux{
    ServeMux: http.NewServeMux(),
  }
}

func (m *MyMux) Use(middlewares ...Middleware) {
  m.middlewares = append(m.middlewares, middlewares...)
}

func (m *MyMux) Handle(pattern string, handler http.Handler) {
  handler = applyMiddlewares(handler, m.middlewares...)
  m.ServeMux.Handle(pattern, handler)
}

func (m *MyMux) HandleFunc(pattern string, handler http.HandlerFunc) {
  newHandler := applyMiddlewares(handler, m.middlewares...)
  m.ServeMux.Handle(pattern, newHandler)
}

When registering, you only need to create the MyMux object, and call its Use() method to pass in the middleware to be applied:

middlewares := []Middleware{
  PanicRecover,
  WithLogger,
  Metric,
}
mux := NewMyMux()
mux.Use(middlewares...)
mux.HandleFunc("/", index)
mux.Handle("/greeting", greeting("welcome, dj"))

This method is simple and easy to use, but it also has its problems. The biggest problem is that the middleware must be set up first, and then Handle/HandleFunc can be called to register. The middleware added later will not take effect on the previously registered processor/function.

In order to solve this problem, we can rewrite the ServeHTTP method and apply the middleware after determining the processor. In this way, the middleware added later can also take effect. Many third-party libraries use this approach. http.ServeMux default ServeHTTP() method of 060ee2209c9961 is as follows:

func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  if r.RequestURI == "*" {
    if r.ProtoAtLeast(1, 1) {
      w.Header().Set("Connection", "close")
    }
    w.WriteHeader(http.StatusBadRequest)
    return
  }
  h, _ := m.Handler(r)
  h.ServeHTTP(w, r)
}

This transformation method definition MyMux type of ServeHTTP() method is very simple, just in m.Handler(r) after acquiring processor, the current middleware application can:

func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // ...
  h, _ := m.Handler(r)
  // 只需要加这一行即可
  h = applyMiddlewares(h, m.middlewares...)
  h.ServeHTTP(w, r)
}

Later, when we analyze the source code of other web frameworks, we will find that many of them are similar. In order to test the crash recovery, write a processing function that will trigger panic:

func panics(w http.ResponseWriter, r *http.Request) {
  panic("not implemented")
}

mux.HandleFunc("/panic", panics)

localhost:8080/ and localhost:8080/greeting in the browser, and finally request localhost:8080/panic trigger panic:


Questions

Thought questions:

This is actually to see if you read the code carefully. The sorted list with the longest prefix is generated ServeMux.Handle()

func (mux *ServeMux) Handle(pattern string, handler Handler) {
  if pattern[len(pattern)-1] == '/' {
    mux.es = appendSorted(mux.es, e)
  }
}

There is obviously a limitation here, that is, the registration path must / before it will be triggered. So both localhost:8080/greeting/a/b/c and localhost:8080/a/b/c will only match the / path. If you want localhost:8080/greeting/a/b/c match the path /greeting , the registration path needs to be changed to /greeting/ :

http.Handle("/greeting/", greeting("Welcome to go web frameworks"))

At this time, the request path /greeting will be automatically redirected (301) to /greeting/ .

to sum up

This article introduces the basic process of using the standard library net/http create a Web server, and analyzes the source code step by step. Then it introduces how to use middleware to simplify general processing logic. Learning and understanding the net/http library is very helpful for learning other Go Web frameworks. Most of the third-party Go Web frameworks implement their own ServeMux objects net/http

If you find a fun and useful Go language library, welcome to submit an issue on the Go Daily Library GitHub😄

reference

  1. Go has a library GitHub every day: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to follow my WeChat public account [GoUpUp], learn together and make progress together~


darjun
2.9k 声望358 粉丝