Go http handler统一响应&异常处理

2021/11/22 探索优化

# 背景

在web开发中,一般会写如下方法, 处理http的请求和响应结果:

// 处理hello请求的handler
func HelloHandler(w http.ResponseWriter, req *http.Request) {
	name := req.URL.Query().Get("name")
	if name == "" { // name 必填判断
		w.Write([]byte("name is required"))
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	data, err := xxService.Find(name)
	if err !=nil{ // 异常响应
		w.Write([]byte(err))
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	b, err := json.Marshal(data)
	if err != nil{ // 反序列化异常
		w.Write([]byte(err))
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.Write(b) // 响应结果
	w.WriteHeader(http.StatusOK)
}

func main() {
  // 注册路由
	http.HandleFunc("/hello", HelloHandler)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

handler的处理代码明显感觉充满“异味”,表现在:

1、职责过重

handler方法中既要处理参数接收、service的调用,还要处理异常和封装响应的结果。

2、代码重复

同一个handler会有多个err需要重复处理,通常也只是简单打印结果和将异常响应给客户端,所以代码类似容易出现重复处理,例如:name为空、反序列化异常、调用service可能出现异常都只是打印

3、无法统一异常处理

每个err都是单独处理,会散落在handler中的不同位置,无法做到统一的异常处理,如果需要调整异常的处理逻辑,例如:打印异常的格式、异常堆栈等, 需要大量调整代码。

4、无法统一响应处理

在开发API时,我们一般会统一响应格式,例如:

type response struct {
		Code    int
		Message string
		Error		string 
		Data    interface{}
}
1
2
3
4
5
6
  • Code:编码,20000表示成功、500xxx表示异常等
  • Message:提示信息
  • Error:异常信息
  • Data:正常响应数据

PS:如果不能统一处理就需要重复在每个handler中创建该结构体,例如:

func HelloHandler(w http.ResponseWriter, req *http.Request) {
	...
	repo := response{
		Code:    200000,
		Message: "",
		Error:   "",
		Data:    nil,
	}
	err := json.NewEncoder(w).Encode(repo)
	if err !=nil{
		log.Error(err)
		return
	}
	w.WriteHeader(http.StatusOK)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 优化方案

将异常处理和响应的逻辑从handler中剥离出来,创建一个统一处理的中间件。

步骤:

首先,调整handler方法的返回值,由原来的无返回结果,改为返回data和异常error,当handler中有遇到异常就直接返回,结果也是直接返回,不再处理。

func HelloHandler(w http.ResponseWriter, req *http.Request) (data interface{}, err error) {
	name := req.URL.Query().Get("name")
	if name == "" {
		return nil, errors.New("name is required")
	}
	return xxService.Find(name)
}
1
2
3
4
5
6
7

PS:调整完后的handler的代码量就会简化很多,也更加清晰。

其次,创建中间件,统一处理异常和响应:

type handler func(w http.ResponseWriter, req *http.Request) (data interface{}, err error)
// 统一处理异常,适配http.HandlerFunc 
func responseHandler(h handler) http.HandlerFunc {
	type response struct {
		Code    int
		Message string
		Data    interface{}
	}
	return func(w http.ResponseWriter, req *http.Request) {
		data, err := h(w, req) // 调用handler方法
		if err != nil { // 异常处理
			log.Error(err)
			w.Write([]byte(err.Error()))
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		resp := response{
			Code:    2000000,
			Message: "success",
			Data:    data,
		}
		// 响应结果处理
		err = json.NewEncoder(w).Encode(resp)
		if err != nil {
			log.Error(err)
			w.Write([]byte(err.Error()))
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusOK)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

最后,调整路由注册,原来直接使用HelloHandler,现在需要包裹一层responseHandler,将异常&响应结果的处理逻辑委托给responseHandler。

func main() {
	http.HandleFunc("/hello", responseHandler(HelloHandler)) // 将改造后的HelloHandler增加一层responseHandler
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
1
2
3
4
5
6
7

这样就实现了将请求参数封装和业务代码调用与响应结果处理的逻辑分离。 但是,每次注册路由都需要用responseHandler包裹我们自定义的handler,这种做法还是会违背了DRY原则,可以考虑进一步优化,将http.HandleFunc进行封装,适配我们自定义的handler:

// 路由。适配自定义的handler
type router func (pattern string, handler func(http.ResponseWriter, *http.Request))

func (r router) HandleFunc(pattern string, handler handler)  {
	r(pattern, responseHandler(handler))
}
1
2
3
4
5
6

PS:router 是一个函数类型,和http.HandleFunc一样,对router附加一个HandleFunc方法,来适配我们自定义的handler

原来代码:

http.HandleFunc("/hello", responseHandler(HelloHandler)) // 将改造后的HelloHandler增加一层responseHandler
1

可以优化为:

r := router(http.HandleFunc)
r.HandleFunc("/hello", HelloHandler) // 不再需要包裹一层 responseHandler
1
2

# 小结

优化后效果:

  • 职责单一:改造后的handler将异常和响应的逻辑剥离出来,职责更加单一。
  • 减少重复代码:消除了重复处理err和响应的代码,更加简洁。
  • 统一响应&异常处理:responseHandler可以对error和响应格式统一处理,如果后续需要额外增加异常处理逻辑或是调整响应格式,只需要修改responseHandler,无需调整其他代码。

该方案同样也适用其他的框架,例如:Gin

func main() {
	r := gin.Default()
  r := NewRouter(e)
	r.GET("/hello", HelloHandler)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
// 处理hello请求的handler。如果有异常返回,响应结果也是直接放回
func HelloHandler(ctx *gin.Context) (data interface{}, err error) {
	name := ctx.Query("name")
	if name == "" {
		return nil, errors.New("name is required")
	}
	return xxService.Find(name)
}

type handler func(ctx *gin.Context) (data interface{}, err error)

type router struct {
	gr gin.IRouter
}

func (r router) GET(s string, h handler) {
	r.gr.GET(s, responseHandler(h))
}

func (r router) POST(s string, h handler)  {
	r.gr.POST(s, responseHandler(h))
}

func (r router) DELETE(s string, h handler)  {
	r.gr.DELETE(s, responseHandler(h))
}
func NewRouter(gr gin.IRouter) *router {
	return &router{
		gr:gr,
	}
}
// 中间件:处理异常和封装响应结果,同时适配gin.HandlerFunc
func responseHandler(h handler) gin.HandlerFunc {
	type response struct {
		Code    int
		Message string
		Data    interface{}
	}
	return func(ctx *gin.Context) {
		data, err := h(ctx)
		if err != nil {
			log.Error(err)
			ctx.Error(err)
			return
		}
		resp := response{
			Code:    2000000,
			Message: "success",
			Data:    data,
		}
		ctx.JSON(http.StatusOK, resp)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62