Go猜想录
大道至简,悟者天成
巧妙的洋葱模型

在 golang 中有强大的标准库支持,我们可以很轻松的写出一个 web 服务。如下所示:

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

func hack(w http.ResponseWriter, r *http.Request) {
	status := struct {
		Status string
	}{
		Status: "OK",
	}

	json.NewEncoder(w).Encode(status)
}

// Output:
// {"Status":"OK"}

可如果需要在已有的逻辑不改变的基础上加一些逻辑,比如说统一打印一些日志或者统一捕获 panic,要如何处理呢?这些逻辑就是抽象出来的中间件,可能很多个 Handler 共享这些中间件。这时,就不能使用默认的 mux 来注册路由了,而需要使用指定的 mux,在注册路由前,在原有的逻辑中添加这些中间件的处理函数后再进行注册,就可以完美解决这个问题。

func main() {
	a := &App{}
	a.Handle(http.MethodGet, "/", hack)
	http.ListenAndServe(":8080", &a.mux)
}

type Handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) error

type App struct {
	mux http.ServeMux
}

func (a *App) Handle(method string, path string, handler Handler) {
	h := func(w http.ResponseWriter, r *http.Request) {

		// 任意代码

		if err := handler(r.Context(), w, r); err != nil {
			return
		}

		// 任意代码
	}

	pattern := method + " " + path

	a.mux.HandleFunc(pattern, h)
}

func hack(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
	status := struct {
		Status string
	}{
		Status: "OK",
	}

	return json.NewEncoder(w).Encode(status)
}

上面只是加了一个中间件进行处理,当有多个中间件时需要一层一层执行,执行流程看起来就像一个洋葱一样。

type Middleware func(Handler) Handler

onion.png

执行过程

onion-1.png

func Logger(handler Handler) Handler {
	h := func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
		// 打日志
		err := handler(r.Context(), w, r)
		// 打日志
		return err
	}
	return h
}

可当有这样一个打日志的中间件,在 Middleware 和 Handler 这两个函数签名都不能改变的情况下要如何把 logger 传入进去呢?当然我们可以将这个函数改成一个方法,在 receiver 中保有一个 logger,不过更优雅简洁的做法是通过闭包的手段将 logger 传入。

func Logger(log *log.Logger) Middleware {
	m := func(handler Handler) Handler {
		h := func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
			// 打日志
			err := handler(r.Context(), w, r)
			// 打日志
			return err
		}
		return h
	}
	return m
}

完整代码

package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"time"
)

func main() {
	a := &App{}
	a.Handle(http.MethodGet, "/", hack, Logger(log.Default()))
	http.ListenAndServe(":8080", &a.mux)
}

func hack(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
	status := struct {
		Status string
		Time   string
	}{
		Status: "OK",
		Time:   time.Now().String(),
	}

	return json.NewEncoder(w).Encode(status)
}

type Handler func(ctx context.Context, w http.ResponseWriter, r *http.Request) error

type App struct {
	mux http.ServeMux
	mw  []Middleware
}

func (a *App) Handle(method string, path string, handler Handler, mw ...Middleware) {
	handler = wrapMiddleware(mw, handler)
	handler = wrapMiddleware(a.mw, handler)

	a.handle(method, path, handler)
}

func (a *App) handle(method string, path string, handler Handler) {
	h := func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if err := handler(ctx, w, r); err != nil {
			// if validateShutdown(err) {
			// 	a.SignalShutdown()
			// 	return
			// }
		}
	}

	pattern := method + " " + path
	a.mux.HandleFunc(pattern, h)
}

type Middleware func(Handler) Handler

func wrapMiddleware(mw []Middleware, handler Handler) Handler {
	for i := len(mw) - 1; i >= 0; i-- {
		mwFunc := mw[i]
		if mwFunc != nil {
			handler = mwFunc(handler)
		}
	}

	return handler
}

func Logger(log *log.Logger) Middleware {
	m := func(handler Handler) Handler {
		h := func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
			// 打日志
			log.Println("start...")
			err := handler(r.Context(), w, r)
			// 打日志
			log.Println("end...")
			return err
		}
		return h
	}
	return m
}

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。