Go http handler統(tǒng)一響應(yīng)&異常處理

背景

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末龙巨,一起剝皮案震驚了整個濱河市笼呆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌旨别,老刑警劉巖诗赌,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秸弛,居然都是意外死亡铭若,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門递览,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叼屠,“玉大人,你說我怎么就攤上這事绞铃【涤辏” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵儿捧,是天一觀的道長荚坞。 經(jīng)常有香客問我,道長菲盾,這世上最難降的妖魔是什么颓影? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮亿汞,結(jié)果婚禮上瞭空,老公的妹妹穿的比我還像新娘。我一直安慰自己疗我,他們只是感情好咆畏,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吴裤,像睡著了一般旧找。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上麦牺,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天钮蛛,我揣著相機與錄音鞭缭,去河邊找鬼。 笑死魏颓,一個胖子當著我的面吹牛岭辣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播甸饱,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼沦童,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了叹话?” 一聲冷哼從身側(cè)響起偷遗,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驼壶,沒想到半個月后氏豌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡热凹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年泵喘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碌嘀。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡涣旨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出股冗,到底是詐尸還是另有隱情霹陡,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布止状,位于F島的核電站烹棉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏怯疤。R本人自食惡果不足惜浆洗,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望集峦。 院中可真熱鬧伏社,春花似錦、人聲如沸塔淤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽高蜂。三九已至聪黎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間备恤,已是汗流浹背稿饰。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工锦秒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人喉镰。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓旅择,卻偏偏與公主長得像,于是被迫代替她去往敵國和親侣姆。 傳聞我的和親對象是個殘疾皇子砌左,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容