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

语言: CN / TW / HK

背景

在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:职责过重

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

问题2:代码重复

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

问题3:无法统一异常处理

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

问题4:无法统一响应处理

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

type response struct {
		Code    int
		Message string
		Error		string 
		Data    interface{}
}
  • 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)
}

优化方案

将异常处理和响应的逻辑从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)
}

调整完后的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)
	}
}

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

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

这样就完成了优化

小结

优化后效果:

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

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

func main() {
	r := gin.Default()
	r.GET("/hello", responseHandler(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)
// 中间件:处理异常和封装响应结果,同时适配gin.HandlerFunc
func response1Handler(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)
	})
}

我的博客: Go http handler统一响应&异常处理 | 艺术码农的小栈