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

2021/11/22 探索优化

# 背景

Golang的在开发web时,会对不同的请求实现不同的hander方法,通常是实现http.HandlerFunc接口:

// 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)
1
2
3
4
5

例如:

package main

import (
	"fmt"
	"net/http"
)

// 处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("name")
	if name == "" {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintln(w, "name is required")
		return
	}
	fmt.Fprintf(w, "Hello, %s!", name)
}

func main() {
	//配置路由
	http.HandleFunc("/", handler)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(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

这里的handler通常充当service的适配器,负责接收请求、校验参数、将参数转换为service方法匹配的格式,然后调用对应service方法,最后将结果响应给客户端。另外,实际开发规范中又会要求统一响应格式,所以还要对响应进行封装,最终hander看起来很杂乱,不那么优雅,总结有以下几点:

1、职责过重

handler方法中既要处理接收参数、校验参数、还负责service的调用,和对返回结果处理。

2、代码重复

handler中存在很多重复的逻辑,比如:正常响应时,需要把格式转换为统一响应格式,异常时要打印异常日志、异常结果封装等。

3、无法统一处理异常

由于err是直接写入http.ResponseWriter,并且会散落在handler中的不同位置,很难做统一的异常处理。

4、无法统一处理响应格式

通常开发规范会约定统一响应格式,例如:

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

如果不能统一处理,就需要重复在每个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

# 改进方案

思路是减轻hander职责,将异常处理和响应处理这部分通用逻辑剥离出来,由一个公共的中间层来处理。减轻职责后,http.HandleFunc接口就可以优化,由于不再处理响应结果,参数http.ResponseWriter就不需要了,只需要约定返回值为两个参数:一个为响应结果,另外一个为异常:

// Handler不再需要调用ResponseWriter方法,交由中间层处理
// data:正常响应结果
// err:异常时返回的错误信息
type HandlerFunc func(req *http.Request) (data any, err error)
1
2
3
4

这样handler调用service方法后,响应内容和异常可以直接返回,就不再需要关心响应结果怎么处理,以及如何响应给客户端:

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

最终的handler代码量就会简化很多,同时也更加清晰。

接下来,就是创建一个中间层适配HandlerAdapt,来统一处理响应结果:

package httpx

import (
	"encoding/json"
	"fmt"
	"net/http"
)

// Handler不再需要调用ResponseWriter方法,交由中间层处理
// data:正常响应结果
// err:异常时返回的错误信息
type HandlerFunc func(req *http.Request) (data any, err error)

type response struct {
	Code    int
	Message string
	Data    interface{}
}

// 统一处理异常,适配http.HandlerFunc
func HandlerAdapt(fn HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		data, err := fn(req)
		if err == nil {
			successHandler(w, data)
		} else {
			errHandler(w, err)
		}

	}
}
// 统一成功处理
func successHandler(w http.ResponseWriter, data any) {
	resp := response{
		Code:    200,
		Message: "success",
		Data:    data,
	}
	w.WriteHeader(http.StatusOK)
	err := json.NewEncoder(w).Encode(resp)
	if err != nil {
		fmt.Printf("json encode error: %v", err)
	}

}
// 统一失败处理
func errHandler(w http.ResponseWriter, err error) {
	resp := response{
		Code:    500,
		Message: "error",
		Data:    err.Error(),
	}
	w.WriteHeader(http.StatusInternalServerError)
	err = json.NewEncoder(w).Encode(resp)
	if err != nil {
		fmt.Printf("json encode error: %v", 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
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

最后,调整路由的注册方法,将自定义的hander用适配器httpx.HandlerAdapt包裹:

package main

import (
	"clean-web/common-handler/good/httpx"
	"net/http"
)
// 处理器函数
func handler(r *http.Request) (data any, err error) {
	...
}

func main() {
	//配置路由
	http.HandleFunc("/", httpx.HandlerAdapt(handler))
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这样就实现了将请求参数封装和业务代码调用与响应结果处理的逻辑分离。 但是,每次注册路由都需要用httpx.HandlerAdapt包裹我们自定义的handler,还是会违背了DRY原则:

//配置路由
http.HandleFunc("/users", httpx.HandlerAdapt(userHandler))
http.HandleFunc("/orders", httpx.HandlerAdapt(orderHandler))
http.HandleFunc("/accounts", httpx.HandlerAdapt(accountHandler))
1
2
3
4

可以考虑进一步优化,自定义一个httpx.HandleFunc(pattern string, handler HandlerFunc)方法来替代原来的http.HandleFunc(...):

package httpx

import "net/http"

func HandleFunc(pattern string, handler HandlerFunc) {
	http.HandleFunc("/users", HandlerAdapt(handler))
}
1
2
3
4
5
6
7

路由配置就可以优化为:

httpx.HandleFunc("/users", userHandler)
httpx.HandleFunc("/orders", orderHandler)
httpx.HandleFunc("/accounts", accountHandler)
1
2
3

到此,我们就完成整体的优化,具体代码可以查看:https://github.com/itart-top/clean-web/tree/main/clean-handler/good。

# 小结

本文介绍了如何通过增加一个中间层,来优化handler的代码逻辑,使其更好的工程化,最终实现:

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

另外,该方案同样也适用其他的框架,例如:Gin,封装后的使用方式如下:

package main

import (
	"clean-web/clean-handler/good-gin/ginx"
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()
	router := ginx.WrapRouter(engine)
	router.GET("/users", userHandler)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(fmt.Sprintf("ListenAndServe: %+v", err))
	}
}

// 处理hello请求的handler。如果有异常返回,响应结果也是直接放回
func userHandler(r *gin.Context) (data any, err error) {
	return fmt.Sprintf("Hello user %s!", r.Param("name")), nil
}

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

(注:完整代码详见:https://github.com/itart-top/clean-web/tree/main/clean-handler/good-gin)