Go 装饰器模式在 API 服务程序中的使用

简介: Golang 开发 API server  Go 语言是由谷歌主导并开源的编程语言,和 C 语言有不少相似之处,都强调执行效率,语言结构尽量简单,也都主要用来解决相对偏底层的问题。
img_b2351bb8e203f6f078c8e6183dac8833.png
Golang 开发 API server

  Go 语言是由谷歌主导并开源的编程语言,和 C 语言有不少相似之处,都强调执行效率,语言结构尽量简单,也都主要用来解决相对偏底层的问题。因为 Go 简洁的语法、较高的开发效率和 goroutine,有一段时间也在 Web 开发上颇为流行。由于工作的关系,我最近也在用 Go 开发 API 服务。但对于 Golang 这种奉行极简主义的语言,如何提高代码复用率就会成为一个很大的挑战,API server 中的大量接口很可能有完全一致的逻辑,如果不解决这个问题,代码会变得非常冗余和难看。

Python 中的装饰器

  在 Python 中,装饰器功能非常好的解决了这个问题,下面的伪代码中展示了一个例子,检查 token 的逻辑放在了装饰器函数 check_token 里,在接口函数上加一个 @check_token 就可以在进入接口函数逻辑前,先检查 token 是否有效。虽然说不用装饰器一样可以将公共逻辑抽取出来,但是调用还是要写在每个接口函数的函数体里,侵入性明显大于使用装饰器的方式。

# 装饰器函数,用来检查客户端的 token 是否有效。
def check_token(): 
    ...

@check_token
# 接口函数,用来让用户登陆。
def login():
    ...

@check_token
# 接口函数,查询用户信息。
def get_user():
    ...

Go 中装饰器的应用

  Go 语言也是可以使用相同的思路来解决这个问题的,但因为 Go 没有提供象 Python 一样便利的语法支持,所以很难做到像 Python 那样漂亮,不过我觉得解决问题才是更重要的,让我们一起来看看是如何做到的吧。

  以下的 API 服务代码示例是基于 Gin-Gonic 框架,对 Gin 不太熟悉的朋友,可以参考我之前翻译的一篇文章:如何使用 Gin 和 Gorm 搭建一个简单的 API 服务器 (一)
  本文中的代码为了方便展示,我做了些简化,完整版见于 https://github.com/blackpiglet/go-api-example

简单示例

  Go 语言实现装饰器的道理并不复杂,CheckParamAndHeader 实现了一个高阶函数,入参 h 是 gin 的基本函数类型 gin.HandlerFunc。返回值是一个匿名函数,类型也是 gin.HandlerFunc。CheckParamAndHeader 中除了运行自己的代码,也调用了作为入参传递进来的 h 函数。

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

  装饰器的功能已经实现了,但如果接口函数需要调用多个装饰,那么函数套函数,还是比较乱,可以写一个装饰器处理函数来简化代码,将装饰器及联起来,这样代码变得简洁了不少。

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")
}

根据接口名称判断用户是否有权限访问

  API 服务程序可能会需要判断用户是否有权限访问接口,如果使用了 MVC 模式,就需要根据接口所在的 module 和接口自己的名称来判断用户能否访问,这就要求在装饰器函数中知道被调用的接口函数名称是什么,这点可以通过 Go 自带的 runtime 库来实现。

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")
}

向装饰器函数传参

  接口可能会有要求客户端必须传某些特定的参数或者消息头,而且很可能每个接口的必传参数都不一样,这就要求装饰器函数可以接收参数,不过我目前还没有找到在 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")
}

  到目前为止,已经实现了我对 API 服务器的基本需求,如果大家有更好的实现方式,烦请赐教,有什么我没想到的需求,也欢迎留言讨论。

  本文主要参考以下两篇文章:
  GO语言的修饰器编程
  Decorated functions in Go
  尤其推荐左耳朵耗子的 GO语言的修饰器编程,里面还谈到了装饰器的范型,让装饰器更加通用。

目录
相关文章
|
12月前
|
JSON 安全 Java
API 一键转换 MCP 服务!Higress 助今日投资快速上线 MCP 市场
今日投资的技术负责人介绍了如何通过Higress MCP 市场完善的解决方案,快捷地将丰富的金融数据 API 转化为 MCP 工具,帮助用户通过 MCP 的方式非常轻松地调用专业金融数据,自由快速地构建自己的金融大模型应用。
1346 23
|
Kubernetes Linux Go
使用 Uber automaxprocs 正确设置 Go 程序线程数
`automaxprocs` 包就是专门用来解决此问题的,并且用法非常简单,只需要使用匿名导入的方式 `import _ "go.uber.org/automaxprocs"` 一行代码即可搞定。
549 78
|
JSON 中间件 Go
Go 网络编程:HTTP服务与客户端开发
Go 语言的 `net/http` 包功能强大,可快速构建高并发 HTTP 服务。本文从创建简单 HTTP 服务入手,逐步讲解请求与响应对象、URL 参数处理、自定义路由、JSON 接口、静态文件服务、中间件编写及 HTTPS 配置等内容。通过示例代码展示如何使用 `http.HandleFunc`、`http.ServeMux`、`http.Client` 等工具实现常见功能,帮助开发者掌握构建高效 Web 应用的核心技能。
571 61
|
开发框架 安全 前端开发
Go Web开发框架实践:模板渲染与静态资源服务
Gin 是一个功能强大的 Go Web 框架,不仅适用于构建 API 服务,还支持 HTML 模板渲染和静态资源托管。它可以帮助开发者快速搭建中小型网站,并提供灵活的模板语法、自定义函数、静态文件映射等功能,同时兼容 Go 的 html/template 引擎,具备高效且安全的页面渲染能力。
|
JSON 编解码 API
Go语言网络编程:使用 net/http 构建 RESTful API
本章介绍如何使用 Go 语言的 `net/http` 标准库构建 RESTful API。内容涵盖 RESTful API 的基本概念及规范,包括 GET、POST、PUT 和 DELETE 方法的实现。通过定义用户数据结构和模拟数据库,逐步实现获取用户列表、创建用户、更新用户、删除用户的 HTTP 路由处理函数。同时提供辅助函数用于路径参数解析,并展示如何设置路由器启动服务。最后通过 curl 或 Postman 测试接口功能。章节总结了路由分发、JSON 编解码、方法区分、并发安全管理和路径参数解析等关键点,为更复杂需求推荐第三方框架如 Gin、Echo 和 Chi。
|
11月前
|
人工智能 算法 API
国产化用于单导联和六导联的心电算法及API服务
随着智能设备普及,心电图功能逐渐应用于智能手表、体脂仪等设备。苏州唯理推出单导联及6导联心电算法API服务,由AI驱动,1分钟内快速评估心律失常、房颤、早搏等问题,已广泛用于医疗设备及三甲医院。其算法还可评估压力、疲劳、情绪状态,筛查效率远超进口设备。唯理率先实现国产医疗级心电芯片,支持快速集成与私有化部署,适用于多种智能硬件。
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
人工智能 API 开发工具
GitHub官方开源MCP服务!GitHub MCP Server:无缝集成GitHub API,实现Git流程完全自动化
GitHub MCP Server是基于Model Context Protocol的服务器工具,提供与GitHub API的无缝集成,支持自动化处理问题、Pull Request和仓库管理等功能。
3433 2
GitHub官方开源MCP服务!GitHub MCP Server:无缝集成GitHub API,实现Git流程完全自动化
|
人工智能 算法 安全
OpenRouter 推出百万 token 上下文 AI 模型!Quasar Alpha:提供完全免费的 API 服务,同时支持联网搜索和多模态交互
Quasar Alpha 是 OpenRouter 推出的预发布 AI 模型,具备百万级 token 上下文处理能力,在代码生成、指令遵循和低延迟响应方面表现卓越,同时支持联网搜索和多模态交互。
1085 1
OpenRouter 推出百万 token 上下文 AI 模型!Quasar Alpha:提供完全免费的 API 服务,同时支持联网搜索和多模态交互
|
人工智能 自然语言处理 API
硅基流动入驻阿里云云市场,核心API服务将全面接入阿里云百炼平台💐
2025年6月18日,AI Infra企业硅基流动与阿里云达成战略合作,加入“繁花计划”并入驻云市场。其大模型推理平台SiliconCloud核心API将接入阿里云百炼平台,依托灵骏智能计算集群为客户提供高效服务。作为国内领先的MaaS平台,SiliconCloud已集成百余款开源大模型,服务600万用户及众多企业。双方将在算力协同、行业解决方案等领域深化合作,推动AI生态发展。
1459 0

热门文章

最新文章