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
- 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~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。