??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語言的修飾器編程睡腿,里面還談到了裝飾器的范型语御,讓裝飾器更加通用。