Go Ginrest实现一个RESTful接口

目录
  • 背景
  • 特性
  • 使用例子
  • 实现原理
  • 功能列表
    • 处理请求
    • 处理响应
    • 处理错误
    • 请求上下文操作
    • 请求结构体处理

背景

基于现在微服务或者服务化的思想,我们大部分的业务逻辑处理函数都是长这样的:

比如grpc服务端:

func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq) (*pb.GetUserInfoRsp, error) {
    // 业务逻辑
    // ...
}

grpc客户端:

func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq, opts ...grpc.CallOption) (*pb.GetUserInfoRsp, error) {
    // 业务逻辑
    // ...
}

有些服务我们需要把它包装为RESTful形式的接口,一般需要经历以下步骤:

  • 指定HTTP方法、URL
  • 鉴权
  • 参数绑定
  • 处理请求
  • 处理响应

可以发现,参数绑定、处理响应几乎都是一样模板代码,鉴权也基本上是模板代码(当然有些鉴权可能比较复杂)。

而Ginrest库就是为了消除这些模板代码,它不是一个复杂的框架,只是一个简单的库,辅助处理这些重复的事情,为了实现这个能力使用了Go1.18的泛型。

仓库地址:github.com/jiaxwu/ginr…

特性

这个库提供以下特性:

  • 封装RESTful请求响应

    • 封装RESTful请求为标准格式服务
    • 封装标准格式服务处理结果为标准RESTful响应格式:Rsp{code, msg, data}
    • 默认使用统一数字错误码格式:[0, 4XXXX, 5XXXX]
    • 默认使用标准错误格式:Error{code, msg}
    • 默认统一状态码[200, 400, 500]
  • 提供Recovery中间件,统一panic时的响应格式
  • 提供SetKey()、GetKey()方法,用于存储请求上下文(泛型)
  • 提供ReqFunc(),用于设置Req(泛型)

使用例子

示例代码在:github.com/jiaxwu/ginr…

首先我们实现两个简单的服务:

const (
	ErrCodeUserNotExists = 40100 // 用户不存在
)
type GetUserInfoReq struct {
	UID int `json:"uid"`
}
type GetUserInfoRsp struct {
	UID      int    `json:"uid"`
	Username string `json:"username"`
	Age      int    `json:"age"`
}
func GetUserInfo(ctx context.Context, req *GetUserInfoReq) (*GetUserInfoRsp, error) {
	if req.UID != 10 {
		return nil, ginrest.NewError(ErrCodeUserNotExists, "user not exists")
	}
	return &GetUserInfoRsp{
		UID:      req.UID,
		Username: "user_10",
		Age:      10,
	}, nil
}
type UpdateUserInfoReq struct {
	UID      int    `json:"uid"`
	Username string `json:"username"`
	Age      int    `json:"age"`
}
type UpdateUserInfoRsp struct{}
func UpdateUserInfo(ctx context.Context, req *UpdateUserInfoReq) (*UpdateUserInfoRsp, error) {
	if req.UID != 10 {
		return nil, ginrest.NewError(ErrCodeUserNotExists, "user not exists")
	}
	return &UpdateUserInfoRsp{}, nil
}

然后使用Gin+Ginrest包装为RESTful接口:

可以看到Register()里面每个接口都只需要一行代码!

func main() {
	e := gin.New()
	e.Use(ginrest.Recovery())
	Register(e)
	if err := e.Run("127.0.0.1:8000"); err != nil {
		log.Println(err)
	}
}
// 注册请求
func Register(e *gin.Engine) {
	// 简单请求,不需要认证
	e.GET("/user/info/get", ginrest.Do(nil, GetUserInfo))
	// 认证,绑定UID,处理
        reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
		req.UID = GetUID(c)
	} // 这里拆多一步是为了显示第一个参数是ReqFunc
	e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
}
const (
	KeyUserID = "KeyUserID"
)
// 简单包装方便使用
func GetUID(c *gin.Context) int {
	return ginrest.GetKey[int](c, KeyUserID)
}
// 简单包装方便使用
func SetUID(c *gin.Context, uid int) {
	ginrest.SetKey(c, KeyUserID, uid)
}
// 认证
func Verify(c *gin.Context) {
	// 认证处理
	// ...
        // 忽略认证的具体逻辑
	SetUID(c, 10)
}

运行上面代码,然后尝试访问接口,可以看到返回结果:

请求1
GET http://127.0.0.1:8000/user/info/get
{
    "uid": 10
}
响应1
{
    "code": 0,
    "msg": "ok",
    "data": {
        "uid": 10,
        "username": "user_10",
        "age": 10
    }
}
请求2
GET http://127.0.0.1:8000/user/info/get
{
    "uid": 1
}
响应2
{
    "code": 40100,
    "msg": "user not exists"
}
请求3
POST http://127.0.0.1:8000/user/info/update
{
    "username": "jiaxwu",
    "age": 10
}
响应3
{
    "code": 0,
    "msg": "ok",
    "data": {}
}

实现原理

Do()和DoOpt()都会转发到do(),它其实是一个模板函数,把脏活累活给处理了:

// 处理请求
func do[Req any, Rsp any, Opt any](reqFunc ReqFunc[Req],
	serviceFunc ServiceFunc[Req, Rsp], serviceOptFunc ServiceOptFunc[Req, Rsp, Opt], opts ...Opt) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 参数绑定
		req, err := BindJSON[Req](c)
		if err != nil {
			return
		}
		// 进一步处理请求结构体
		if reqFunc != nil {
			reqFunc(c, req)
		}
		var rsp *Rsp
		// 业务逻辑函数调用
		if serviceFunc != nil {
			rsp, err = serviceFunc(c, req)
		} else if serviceOptFunc != nil {
			rsp, err = serviceOptFunc(c, req, opts...)
		} else {
			panic("must one of ServiceFunc and ServiceFuncOpt")
		}
		// 处理响应
		ProcessRsp(c, rsp, err)
	}
}

功能列表

处理请求

用于把一个标准服务封装为一个RESTfulgin.HandlerFunc,对应Do()、DoOpt()函数。

DoOpt()相比于Do()多了一个opts参数,因为很多rpc框架客户端都有一个opts参数作为结尾。

还有一个BindJSON(),用于把请求体包装为一个Req结构体:

// 参数绑定
func BindJSON[T any](c *gin.Context) (*T, error) {
	var req T
	if err := c.ShouldBindJSON(&req); err != nil {
		FailureCodeMsg(c, ErrCodeInvalidReq, "invalid param")
		return nil, err
	}
	return &req, nil
}

如果无法使用Do()和DoOpt()则可以使用此方法。

处理响应

用于把rsp、error、errcode、errmsg等数据封装为一个JSON格式响应体,对应ProcessRsp()、Success()、Failure()、FailureCodeMsg()函数。

比如ProcessRsp()需要带上rsp和error,这样业务里面就不需要再写如下模板代码了:

// 处理简单响应
func ProcessRsp(c *gin.Context, rsp any, err error) {
	if err != nil {
		Failure(c, err)
		return
	}
	Success(c, rsp)
}

响应格式统一为:

// 响应
type Rsp struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data any    `json:"data,omitempty"`
}

Success()用于处理成功情况:

// 请求成功
func Success(c *gin.Context, data any) {
	ginRsp(c, http.StatusOK, &Rsp{
		Code: ErrCodeOK,
		Msg:  "ok",
		Data: data,
	})
}

其余同理。

如果无法使用Do()和DoOpt()则可以使用这些方法。

处理错误

一般我们都需要在出错时带上一个业务错误码,方便客户端处理。因此我们需要提供一个合适的error类型:

// 错误
type Error struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

我们提供了一些函数方便使用Error,对应NewError()、ToError()、ErrCode()、ErrMsg()、ErrEqual()函数。

比如NewError()生成一个Error类型error:

// 通过code和msg产生一个错误
func NewError(code int, msg string) error {
	return &Error{
		Code: code,
		Msg:  msg,
	}
}

请求上下文操作

Gin的请求是链式处理的,也就是多个handler顺序的处理一个请求,比如:

        reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
		req.UID = ginrest.GetKey[int](c, KeyUserID)
	}
        // 认证,绑定UID,处理
	e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))

这个接口经历了Verify和ginrest.Do两个handler,其中我们在Verify的时候通过认证知道了用户的身份信息(比如uid),我们希望把这个uid存起来,这样可以在业务逻辑里使用。

因此我们提供了SetKey()、GetKey()两个函数,用于存储请求上下文:

比如认证通过后我们可以设置UID到上下文,然后在reqFunc()里读取设置到req里面(下面介绍)。

// 认证
func Verify(c *gin.Context) {
	// 认证处理
	// ...
	// 忽略认证的具体逻辑
	ginrest.SetKey(c, KeyUserID, uid)
}

请求结构体处理

上面我们设置了请求上下文,比如UID,但是其实我们并不知道具体这个UID是需要设置到req里的哪个字段,因此我们提供了一个回调函数ReqFunc(),用于设置Req:

	// 这里↓
        reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
		req.UID = ginrest.GetKey[int](c, KeyUserID)
	}
        // 认证,绑定UID,处理
	e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))

如果这个库的设计不符合具体的业务,也可以按照这种思路去封装一个类似的库,只要尽可能的统一请求、响应的格式,就可以减少很多重复的模板代码。

以上就是Go Ginrest实现一个RESTful接口的详细内容,更多关于Go Ginrest实现RESTful接口的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解Go-JWT-RESTful身份认证教程

    1.什么是JWT JWT(JSON Web Token)是一个非常轻巧的规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息, 一个JWT由三部分组成,Header头部.Claims载荷.Signature签名, JWT原理类似我们加盖公章或手写签名的的过程,合同上写了很多条款,不是随便一张纸随便写啥都可以的,必须要一些证明,比如签名,比如盖章,JWT就是通过附加签名,保证传输过来的信息是真的,而不是伪造的, 它将用户信息加密到token里,服务器不保存任何用户信息,服务器通过使

  • 详解Go语言RESTful JSON API创建

    RESTful API在Web项目开发中广泛使用,本文针对Go语言如何一步步实现RESTful JSON API进行讲解, 另外也会涉及到RESTful设计方面的话题. 也许我们之前有使用过各种各样的API, 当我们遇到设计很糟糕的API的时候,简直感觉崩溃至极.希望通过本文之后,能对设计良好的RESTful API有一个初步认识. JSON API是什么? JSON之前,很多网站都通过XML进行数据交换.如果在使用过XML之后,再接触JSON, 毫无疑问,你会觉得世界多么美好.这里不深入JSO

  • 浅谈go-restful框架的使用和实现

    REST(Representational State Transfer,表现层状态转化)是近几年使用较广泛的分布式结点间同步通信的实现方式.REST原则描述网络中client-server的一种交互形式,即用URL定位资源,用HTTP方法描述操作的交互形式.如果CS之间交互的网络接口满足REST风格,则称为RESTful API.以下是 理解RESTful架构 总结的REST原则: 网络上的资源通过URI统一标示. 客户端和服务器之间传递,这种资源的某种表现层.表现层可以是json,文本,二进

  • golang 实现一个restful微服务的操作

    如何用net/http构建一个简单的web服务 Golang提供了简洁的方法来构建web服务 package main import ( "net/http" ) func HelloResponse(rw http.ResponseWriter, request *http.Request) { fmt.Fprintf(w, "Hello world.") } func main() { http.HandleFunc("/", HelloRe

  • go 原生http web 服务跨域restful api的写法介绍

    错误写法 func main() { openHttpListen() } func openHttpListen() { http.HandleFunc("/", receiveClientRequest) fmt.Println("go server start running...") err := http.ListenAndServe(":9090", nil) if err != nil { log.Fatal("Liste

  • 基于Go语言构建RESTful API服务

    目录 什么是 RESTful API 一个简单的 RESTful API RESTful JSON API Gin 框架 引入 Gin 框架 使用 Gin 框架 新增一个用户 获取特定的用户 总结 在实际开发项目中,你编写的服务可以被其他服务使用,这样就组成了微服务的架构:也可以被前端调用,这样就可以前后端分离.那么,本文主要介绍什么是 RESTful API,以及 Go 语言是如何玩转 RESTful API 的. 什么是 RESTful API RESTful API 是一套规范,它可以规范

  • Go Ginrest实现一个RESTful接口

    目录 背景 特性 使用例子 实现原理 功能列表 处理请求 处理响应 处理错误 请求上下文操作 请求结构体处理 注 背景 基于现在微服务或者服务化的思想,我们大部分的业务逻辑处理函数都是长这样的: 比如grpc服务端: func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq) (*pb.GetUserInfoRsp, error) { // 业务逻辑 // ... } grpc客户端: func (s *S

  • spring常用注解开发一个RESTful接口示例

    目录 一.开发REST接口 1.第一步:定义资源(对象) 2.第二步:HTTP方法与Controller(动作) 二.统一规范接口响应的数据格式 一.开发REST接口 在本专栏之前的章节中已经给大家介绍了 Spring常用注解及http数据转换教程 Spring Boot提高开发效率必备工具lombok使用 Spring Boot开发RESTful接口与http协议状态表述 本节内容就是将之前学到的内容以代码的方式体现出来. 1. 第一步:定义资源(对象) @Data @Builder publ

  • PHP编写RESTful接口

    首先我们来认识下RESTful Restful是一种设计风格而不是标准,比如一个接口原本是这样的: http://www.test.com/user/view/id/1 表示获取id为1的用户信息,如果使用Restful风格,可以变成这样: http://www.test.com/user/1 可以很明显的看出这样做的好处: 1.更简洁的URL,对程序员友好 2.不暴露内部代码结构,更安全 那么,如何实现这个接口呢?首先,我们需要接收到/user/1部分. $path = $_SERVER['P

  • 基于Restful接口调用方法总结(超详细)

    由于在实际项目中碰到的restful服务,参数都以json为准.这里我获取的接口和传入的参数都是json字符串类型.发布restful服务可参照文章 Jersey实现Restful服务(实例讲解),以下接口调用基于此服务. 基于发布的Restful服务,下面总结几种常用的调用方法. (1)Jersey API package com.restful.client; import com.fasterxml.jackson.core.JsonProcessingException; import

  • PHP编写RESTful接口的方法

    这是一个轻量级框架,专为快速开发RESTful接口而设计.如果你和我一样,厌倦了使用传统的MVC框架编写微服务或者前后端分离的API接口,受不了为了一个简单接口而做的很多多余的coding(和CTRL-C/CTRL-V),那么,你肯定会喜欢这个框架! 先举个栗子 1.写个HelloWorld.php,放到框架指定的目录下(默认是和index.php同级的apis/目录) /** * @path("/hw") */ class HelloWorld { /** * @route({&qu

  • 使用python为mysql实现restful接口

    最近在做游戏服务分层的时候,一直想把mysql的访问独立成一个单独的服务DBGate,原因如下: 请求收拢到DBGate,可以使DBGate变为无状态的,方便横向扩展 当请求量或者存储量变大时,mysql需要做分库分表,DBGate可以内部直接处理,外界无感知 通过restful限制对数据请求的形式,仅支持简单的get/post/patch/put 进行增删改查,并不支持复杂查询.这个也是和游戏业务的特性有关,如果网站等需要复杂查询的业务,对此并不适合 DBGate使用多进程模式,方便控制与my

  • Spring Boot构建优雅的RESTful接口过程详解

    RESTful 相信在座的各位对于RESTful都是略有耳闻,那么RESTful到底是什么呢? REST(Representational State Transfer)表述性状态转移是一组架构约束条件和原则.满足这些约束条件和原则的应用程序或设计就是RESTful.需要注意的是,REST是设计风格而不是标准.REST通常基于使用HTTP,URI,和XML(标准通用标记语言下的一个子集)以及HTML(标准通用标记语言下的一个应用)这些现有的广泛流行的协议和标准. 也许这段话有些晦涩难懂,换个角度

  • SpringBoot框架RESTful接口设置跨域允许

    跨域 跨域请求是指浏览器脚本文件在发送请求时,脚本所在的服务器和请求的服务器地址不一样.跨域是有浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制, 同源策略:是指协议.域名.端口都要相同,其中有一个不同都会产生跨域 SpringBoot框架RESTful接口解决跨域 此处是有配置文件的方式来解决的 package com.prereadweb.config.cors; import org.springframework.context.annotation.Bean; im

  • Jersey Restful接口如何获取参数的问题

    缘起 工作时使用java开发服务器后台,用Jersey写Restful接口,发现有一个Post方法始终获取不到参数,查了半天,发现时获取参数的注释不太对,将@formparam写成了@queryparam,发现了这个改过来就好了,顺便整理了一下不同参数的作用. 简述 获取URI的参数 获取Get请求的参数 获取Post类型的参数 添加参数默认值 获取Map参数 1.@PathParam 使用该注释获取参数时可以获取URI中制定规则的参数 例如: //该类的路径为/user @GET @Path(

  • Spring Boot开发RESTful接口与http协议状态表述

    目录 一.RESTful风格API的好处 二.RESTfulAPI的设计风格 2.1.RESTful是面向资源的(名词) 2.2.用HTTP方法体现对资源的操作(动词) 2.3.HTTP状态码 2.4.Get方法和查询参数不应该改变数据 2.5.使用复数名词 2.6.复杂资源关系的表达 2.7.高级用法:HATEOAS 2.8.资源过滤.排序.选择和分页的表述 2.9.版本化你的API 参考: 一.RESTful风格API的好处 API(Application Programming Inter

随机推荐