Go 裝飾器模式在 API 服務程序中的使用

Golang 開發(fā) API server

??Go 語言是由谷歌主導并開源的編程語言,和 C 語言有不少相似之處,都強調(diào)執(zhí)行效率抢腐,語言結(jié)構(gòu)盡量簡單跛璧,也都主要用來解決相對偏底層的問題。因為 Go 簡潔的語法歇拆、較高的開發(fā)效率和 goroutine鞋屈,有一段時間也在 Web 開發(fā)上頗為流行。由于工作的關(guān)系查吊,我最近也在用 Go 開發(fā) API 服務谐区。但對于 Golang 這種奉行極簡主義的語言,如何提高代碼復用率就會成為一個很大的挑戰(zhàn)逻卖,API server 中的大量接口很可能有完全一致的邏輯宋列,如果不解決這個問題,代碼會變得非常冗余和難看评也。

Python 中的裝飾器

??在 Python 中炼杖,裝飾器功能非常好的解決了這個問題灭返,下面的偽代碼中展示了一個例子,檢查 token 的邏輯放在了裝飾器函數(shù) check_token 里坤邪,在接口函數(shù)上加一個 @check_token 就可以在進入接口函數(shù)邏輯前熙含,先檢查 token 是否有效。雖然說不用裝飾器一樣可以將公共邏輯抽取出來艇纺,但是調(diào)用還是要寫在每個接口函數(shù)的函數(shù)體里怎静,侵入性明顯大于使用裝飾器的方式。

# 裝飾器函數(shù)黔衡,用來檢查客戶端的 token 是否有效蚓聘。
def check_token(): 
    ...

@check_token
# 接口函數(shù),用來讓用戶登陸盟劫。
def login():
    ...

@check_token
# 接口函數(shù)夜牡,查詢用戶信息。
def get_user():
    ...

Go 中裝飾器的應用

??Go 語言也是可以使用相同的思路來解決這個問題的侣签,但因為 Go 沒有提供象 Python 一樣便利的語法支持塘装,所以很難做到像 Python 那樣漂亮,不過我覺得解決問題才是更重要的影所,讓我們一起來看看是如何做到的吧蹦肴。

??以下的 API 服務代碼示例是基于 Gin-Gonic 框架,對 Gin 不太熟悉的朋友型檀,可以參考我之前翻譯的一篇文章:如何使用 Gin 和 Gorm 搭建一個簡單的 API 服務器 (一)
??本文中的代碼為了方便展示冗尤,我做了些簡化,完整版見于 https://github.com/blackpiglet/go-api-example

簡單示例

??Go 語言實現(xiàn)裝飾器的道理并不復雜胀溺,CheckParamAndHeader 實現(xiàn)了一個高階函數(shù)裂七,入?yún)?h 是 gin 的基本函數(shù)類型 gin.HandlerFunc。返回值是一個匿名函數(shù)仓坞,類型也是 gin.HandlerFunc背零。CheckParamAndHeader 中除了運行自己的代碼,也調(diào)用了作為入?yún)鬟f進來的 h 函數(shù)无埃。

package main

import (
        "fmt"

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

func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc {
        return func(c *gin.Context) {
                header := c.Request.Header.Get("token")
                if header == "" {
                        c.JSON(200, gin.H{
                                "code":   3,  
                                "result": "failed",
                                "msg":    ". Missing token",
                        })  
                        return
                }   
        }   
}

func Login(c *gin.Context) {
        c.JSON(200, gin.H{
                "code":   0,  
                "result": "success",
                "msg":    "驗證成功",
        })  
}

func main() {
        r := gin.Default()
        r.POST("/v1/login", CheckParamAndHeader(Login))
        r.Run(":8080")
}

裝飾器的 pipeline

??裝飾器的功能已經(jīng)實現(xiàn)了徙瓶,但如果接口函數(shù)需要調(diào)用多個裝飾,那么函數(shù)套函數(shù)嫉称,還是比較亂侦镇,可以寫一個裝飾器處理函數(shù)來簡化代碼,將裝飾器及聯(lián)起來织阅,這樣代碼變得簡潔了不少壳繁。

package main

import (
    "fmt"

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

func Decorator(h gin.HandlerFunc, decors ...HandlerDecoratored) gin.HandlerFunc {
    for i := range decors {
        d := decors[len(decors)-1-i] // iterate in reverse
        h = d(h)
    }
    return h
}

func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc {
    return func(c *gin.Context) {
        header := c.Request.Header.Get("token")
        if header == "" {
            c.JSON(200, gin.H{
                "code":   3,
                "result": "failed",
                "msg":    ". Missing token",
            })
            return
        }
    }
}

func CheckParamAndHeader_1(h gin.HandlerFunc) gin.HandlerFunc {
    return func(c *gin.Context) {
        header := c.Request.Header.Get("auth")
        if header == "" {
            c.JSON(200, gin.H{
                "code":   3,
                "result": "failed",
                "msg":    ". Missing auth",
            })
            return
        }
    }
}

func Login(c *gin.Context) {
    c.JSON(200, gin.H{
        "code":   0,
        "result": "success",
        "msg":    "驗證成功",
    })
}

func main() {
    r := gin.Default()
    r.POST("/v1/login", Decorator(CheckParamAndHeader, CheckParamAndHeader_1, Login))
    r.Run(":8080")
}

根據(jù)接口名稱判斷用戶是否有權(quán)限訪問

??API 服務程序可能會需要判斷用戶是否有權(quán)限訪問接口,如果使用了 MVC 模式,就需要根據(jù)接口所在的 module 和接口自己的名稱來判斷用戶能否訪問闹炉,這就要求在裝飾器函數(shù)中知道被調(diào)用的接口函數(shù)名稱是什么蒿赢,這點可以通過 Go 自帶的 runtime 庫來實現(xiàn)。

package main

import (
    "fmt"
    "runtime"
    "strings"

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

func Decorator(h gin.HandlerFunc, decors ...HandlerDecoratored) gin.HandlerFunc {
    for i := range decors {
        d := decors[len(decors)-1-i] // iterate in reverse
        h = d(h)
    }
    return h
}

func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc {
    return func(c *gin.Context) {
        header := c.Request.Header.Get("token")
        if header == "" {
            c.JSON(200, gin.H{
                "code":   3,
                "result": "failed",
                "msg":    "Missing token",
            })
            return
        }
    }
}

func CheckPermission(h gin.HandlerFunc) gin.HandlerFunc {
    return func(c *gin.Context) {
        function_name_str := runtime.FuncForPC(reflect.ValueOf(input).Pointer()).Name()

        function_name_array := strings.Split(function_name_str, "/")
        module_method := strings.Split(function_name_array[len(function_name_array)-1], ".")
        module := module_method[0]
        method := module_method[1]

        if module != "Login" {
            c.JSON(200, gin.H{
                "code":   2,
                "result": "failed",
                "msg":    "No permission",
            })
            return
        }
    }
}

func Login(c *gin.Context) {
    c.JSON(200, gin.H{
        "code":   0,
        "result": "success",
        "msg":    "驗證成功",
    })
}

func main() {
    r := gin.Default()
    r.POST("/v1/login", Decorator(CheckParamAndHeader, CheckPermission, Login))
    r.Run(":8080")
}

向裝飾器函數(shù)傳參

??接口可能會有要求客戶端必須傳某些特定的參數(shù)或者消息頭渣触,而且很可能每個接口的必傳參數(shù)都不一樣羡棵,這就要求裝飾器函數(shù)可以接收參數(shù),不過我目前還沒有找到在 pipeline 的方式下傳參的方法嗅钻,只能使用最基本的方式皂冰。

package main

import (
    "fmt"
    "runtime"
    "strconv"
    "strings"

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

func CheckParamAndHeader(input gin.HandlerFunc, http_params ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        http_params_local := append([]string{"param:user_id", "header:token"}, http_params...)
        required_params_str := strings.Join(http_params_local, ", ")
        required_params_str = "Required parameters include: " + required_params_str
        fmt.Println(http_params_local, required_params_str, len(http_params_local))

        for _, v := range http_params_local {
            ret := strings.Split(v, ":")

            switch ret[0] {
            case "header":
                header := c.Request.Header.Get(ret[1])

                if header == "" {
                    c.JSON(200, gin.H{
                        "code":   3,
                        "result": "failed",
                        "msg":    required_params_str + ". Missing " + v,
                    })
                    return
                }
            case "param":
                _, err := c.GetQuery(ret[1])
                if err == false {
                    c.JSON(200, gin.H{
                        "code":   3,
                        "result": "failed",
                        "msg":    required_params_str + ". Missing " + v,
                    })
                    return

                }
            case "body":
                body_param := c.PostForm(ret[1])

                if body_param == "" {
                    c.JSON(200, gin.H{
                        "code":   3,
                        "result": "failed",
                        "msg":    required_params_str + ". Missing " + v,
                    })
                    return
                }
            default:
                fmt.Println("Unsupported checking type: %s", ret[0])
            }
        }
        input(c)
    }
}

func CheckPermission(h gin.HandlerFunc) gin.HandlerFunc {
    return func(c *gin.Context) {
        function_name_str := runtime.FuncForPC(reflect.ValueOf(input).Pointer()).Name()

        function_name_array := strings.Split(function_name_str, "/")
        module_method := strings.Split(function_name_array[len(function_name_array)-1], ".")
        module := module_method[0]
        method := module_method[1]

        if module != "Login" {
            c.JSON(200, gin.H{
                "code":   2,
                "result": "failed",
                "msg":    "No permission",
            })
            return
        }
    }
}

func Login(c *gin.Context) {
    c.JSON(200, gin.H{
        "code":   0,
        "result": "success",
        "msg":    "驗證成功",
    })
}

func main() {
    r := gin.Default()
    r.POST("/v1/login", CheckParamAndHeader(CheckPermission(Login), "body:password", "body:name"))
    r.Run(":8080")
}

??到目前為止,已經(jīng)實現(xiàn)了我對 API 服務器的基本需求养篓,如果大家有更好的實現(xiàn)方式灼擂,煩請賜教,有什么我沒想到的需求觉至,也歡迎留言討論。

??本文主要參考以下兩篇文章:
??GO語言的修飾器編程
??Decorated functions in Go
??尤其推薦左耳朵耗子的 GO語言的修飾器編程睡腿,里面還談到了裝飾器的范型语御,讓裝飾器更加通用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末席怪,一起剝皮案震驚了整個濱河市应闯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌挂捻,老刑警劉巖碉纺,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異刻撒,居然都是意外死亡骨田,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門声怔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來态贤,“玉大人,你說我怎么就攤上這事醋火∮破” “怎么了?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵芥驳,是天一觀的道長柿冲。 經(jīng)常有香客問我,道長兆旬,這世上最難降的妖魔是什么假抄? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上慨亲,老公的妹妹穿的比我還像新娘婚瓜。我一直安慰自己,他們只是感情好刑棵,可當我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布巴刻。 她就那樣靜靜地躺著,像睡著了一般蛉签。 火紅的嫁衣襯著肌膚如雪胡陪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天碍舍,我揣著相機與錄音柠座,去河邊找鬼。 笑死片橡,一個胖子當著我的面吹牛妈经,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捧书,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼吹泡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了经瓷?” 一聲冷哼從身側(cè)響起爆哑,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舆吮,沒想到半個月后揭朝,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡色冀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年潭袱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锋恬。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡敌卓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出伶氢,到底是詐尸還是另有隱情趟径,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布癣防,位于F島的核電站蜗巧,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蕾盯。R本人自食惡果不足惜幕屹,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧望拖,春花似錦渺尘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盔沫,卻和暖如春医咨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背架诞。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工拟淮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谴忧。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓很泊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親沾谓。 傳聞我的和親對象是個殘疾皇子撑蚌,可洞房花燭夜當晚...
    茶點故事閱讀 44,974評論 2 355