背景
Golang的在開發(fā)web時,會對不同的請求實現(xiàn)不同的hander方法单雾,通常是實現(xiàn)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)
例如:
package main
import (
"fmt"
"net/http"
)
// 處理器函數(shù)
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)
}
}
這里的handler通常充當service的適配器践啄,負責接收請求浇雹、校驗參數(shù)、將參數(shù)轉(zhuǎn)換為service方法匹配的格式屿讽,然后調(diào)用對應(yīng)service方法昭灵,最后將結(jié)果響應(yīng)給客戶端。另外伐谈,實際開發(fā)規(guī)范中又會要求統(tǒng)一響應(yīng)格式烂完,所以還要對響應(yīng)進行封裝,最終hander看起來很雜亂诵棵,不那么優(yōu)雅抠蚣,總結(jié)有以下幾點:
1、職責過重
handler方法中既要處理接收參數(shù)非春、校驗參數(shù)柱徙、還負責service的調(diào)用,和對返回結(jié)果處理奇昙。
2护侮、代碼重復(fù)
handler中存在很多重復(fù)的邏輯,比如:正常響應(yīng)時储耐,需要把格式轉(zhuǎn)換為統(tǒng)一響應(yīng)格式羊初,異常時要打印異常日志、異常結(jié)果封裝等什湘。
3长赞、無法統(tǒng)一處理異常
由于err是直接寫入http.ResponseWriter,并且會散落在handler中的不同位置闽撤,很難做統(tǒng)一的異常處理得哆。
4、無法統(tǒng)一處理響應(yīng)格式
通常開發(fā)規(guī)范會約定統(tǒng)一響應(yīng)格式哟旗,例如:
type response struct {
Code int
Message string
Error string
Data interface{}
}
- Code:編碼贩据,20000表示成功栋操、500xxx表示異常等
- Message:提示信息
- Error:異常信息
- Data:正常響應(yīng)數(shù)據(jù)
如果不能統(tǒng)一處理,就需要重復(fù)在每個handler中構(gòu)造響應(yīng)的結(jié)構(gòu)體饱亮,例如:
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)
}
改進方案
思路是減輕hander職責矾芙,將異常處理和響應(yīng)處理這部分通用邏輯剝離出來,由一個公共的中間層來處理近上。減輕職責后剔宪,http.HandleFunc接口就可以優(yōu)化,由于不再處理響應(yīng)結(jié)果壹无,參數(shù)http.ResponseWriter就不需要了葱绒,只需要約定返回值為兩個參數(shù):一個為響應(yīng)結(jié)果,另外一個為異常:
// Handler不再需要調(diào)用ResponseWriter方法格遭,交由中間層處理
// data:正常響應(yīng)結(jié)果
// err:異常時返回的錯誤信息
type HandlerFunc func(req *http.Request) (data any, err error)
這樣handler調(diào)用service方法后哈街,響應(yīng)內(nèi)容和異常可以直接返回拒迅,就不再需要關(guān)心響應(yīng)結(jié)果怎么處理骚秦,以及如何響應(yīng)給客戶端:
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代碼量就會簡化很多,同時也更加清晰璧微。
接下來作箍,就是創(chuàng)建一個中間層適配HandlerAdapt,來統(tǒng)一處理響應(yīng)結(jié)果:
package httpx
import (
"encoding/json"
"fmt"
"net/http"
)
// Handler不再需要調(diào)用ResponseWriter方法前硫,交由中間層處理
// data:正常響應(yīng)結(jié)果
// err:異常時返回的錯誤信息
type HandlerFunc func(req *http.Request) (data any, err error)
type response struct {
Code int
Message string
Data interface{}
}
// 統(tǒng)一處理異常胞得,適配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)
}
}
}
// 統(tǒng)一成功處理
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)
}
}
// 統(tǒng)一失敗處理
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)
}
}
最后,調(diào)整路由的注冊方法屹电,將自定義的hander用適配器httpx.HandlerAdapt包裹:
package main
import (
"clean-web/common-handler/good/httpx"
"net/http"
)
// 處理器函數(shù)
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)
}
}
這樣就實現(xiàn)了將請求參數(shù)封裝和業(yè)務(wù)代碼調(diào)用與響應(yīng)結(jié)果處理的邏輯分離阶剑。
但是,每次注冊路由都需要用httpx.HandlerAdapt包裹我們自定義的handler危号,還是會違背了DRY原則:
//配置路由
http.HandleFunc("/users", httpx.HandlerAdapt(userHandler))
http.HandleFunc("/orders", httpx.HandlerAdapt(orderHandler))
http.HandleFunc("/accounts", httpx.HandlerAdapt(accountHandler))
可以考慮進一步優(yōu)化牧愁,自定義一個httpx.HandleFunc(pattern string, handler HandlerFunc)方法來替代原來的http.HandleFunc(...):
package httpx
import "net/http"
func HandleFunc(pattern string, handler HandlerFunc) {
http.HandleFunc("/users", HandlerAdapt(handler))
}
路由配置就可以優(yōu)化為:
httpx.HandleFunc("/users", userHandler)
httpx.HandleFunc("/orders", orderHandler)
httpx.HandleFunc("/accounts", accountHandler)
到此,我們就完成整體的優(yōu)化外莲,具體代碼可以查看:https://github.com/itart-top/clean-web/tree/main/clean-handler/good猪半。
小結(jié)
本文介紹了如何通過增加一個中間層,來優(yōu)化handler的代碼邏輯偷线,使其更好的工程化磨确,最終實現(xiàn):
- 職責單一:改造后的handler將異常和響應(yīng)的邏輯剝離出來,職責更加單一声邦。
- 減少重復(fù)代碼:消除了重復(fù)處理err和響應(yīng)的代碼乏奥,更加簡潔。
- 統(tǒng)一響應(yīng)&異常處理:HandlerAdapt統(tǒng)一處理結(jié)果亥曹,后續(xù)需要額外增加異常處理邏輯或是調(diào)整響應(yīng)格式邓了,只需要修改該層邏輯盏檐,無需調(diào)整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痕鳍。如果有異常返回,響應(yīng)結(jié)果也是直接放回
func userHandler(r *gin.Context) (data any, err error) {
return fmt.Sprintf("Hello user %s!", r.Param("name")), nil
}
(注:完整代碼詳見:https://github.com/itart-top/clean-web/tree/main/clean-handler/good-gin)
轉(zhuǎn)自: https://itart.cn/blogs/2021/explore/go-http-response-uniform-handler.html