Go语言服务、请求、响应、错误码设计与实现 一般的请求响应格

一般的请求响应格式

对于现在大多数系统来说,请求一般就是原始请求参数的JSON格式+HTTP头的Token,响应一般会带上错误码和错误信息,封装为【错误码,错误信息,数据】的JSON格式,请求和响应都会放在HTTP的Body。

对于错误码,常见的选择有数字,字母,和字母+数字这几种方式。

对于HTTP方法,常见的选择有统一POST,获取操作用GET+修改操作用POST,还有RESTful风格的GET、POST、DELETE、PUT等。

对于HTTP状态码,常见的使用方式有统一200,或者使用200、400、401、403、404、500等常见状态码。

对于HTTP头,一般会放置一些权限相关的信息,比如Token。

实现

我们的实现错误码选择字母格式,HTTP方法统一POST,请求参数都放在Body(Token等除外),HTTP状态码使用统一200,使用Gin框架实现。

Service格式

Service是业务逻辑实现的地方,在有异常体系的语言里,我们经常使用:服务名(请求) 响应 throws 异常这种结构。但是Go一般使用error表示错误,因此Go的服务结构为:服务名(请求) (响应, 错误)

比如:登录服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码package login

import (
"github.com/google/uuid"
)

type Service struct{}

func NewService() *Service {
return &Service{}
}

func (s *Service) Login(req *LoginReq) (*LoginRsp, error) {
if req.Username != "admin" || req.Password != "admin" {
return nil, ErrCodeInvalidParameterUsernameOrPassword
}

return &LoginRsp{
Token: uuid.New().String(),
}, nil
}

其中req和rsp都是简单的结构体:

1
2
3
4
5
6
7
8
9
10
go复制代码package login

type LoginReq struct {
Username string `json:"username"`
Password string `json:"password"`
}

type LoginRsp struct {
Token string `json:"token"`
}

而ErrCodeInvalidParameterUsernameOrPassword是一个ErrCode类型的实例:

1
2
3
4
5
6
7
8
go复制代码package login

import "github.com/jiaxwu/go_service_req_rsp_errcode_demo/service/common"

var (
ErrCodeInvalidParameterUsernameOrPassword = common.NewErrCode("InvalidParameterUsernameOrPassword",
"The username or password is not valid.", "账号或密码错误")
)

这里可以直接返回错误码是因为错误码实现了error接口,下面给出ErrCode的定义。

错误码实现

ErrCode使用了三个字段,错误码、错误信息(给开发者看)和建议(给用户看)。这里ErrCode实现了error接口,因此我们才可以把它当成error类型使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
go复制代码package common

import (
"fmt"
)

var (
ErrCodeOK = NewErrCode("OK", "", "")

ErrCodeInvalidParameter = NewErrCode("InvalidParameter", "the required parameter is not validate",
"非法参数")

ErrCodeForbidden = NewErrCode("Forbidden", "forbidden", "无权访问")

ErrCodeNotFound = NewErrCode("NotFound", "the request resource not found", "找不到要访问的资源")

ErrCodeInternalError = NewErrCode("InternalError",
"the request processing has failed due to some unknown error", "给您带来的不便,深感抱歉,请稍后再试")
)

// ErrCode 错误码
type ErrCode struct {
Code string `json:"Code"`
Msg string `json:"Msg"`
Advice string `json:"Advice"`
}

func (e *ErrCode) Error() string {
return fmt.Sprintf("code: %s, msg: %s, advice: %s", e.Code, e.Msg, e.Advice)
}

// NewErrCode 新建一个错误码
func NewErrCode(code, msg, advice string) *ErrCode {
return &ErrCode{
Code: code,
Msg: msg,
Advice: advice,
}
}

这里在最上面定义了一些全局通用的错误码,这样不用每个服务都重复定义这些错误码。

HTTP响应

对于HTTP响应,我们不仅需要带上Service的结果,还需要带上HTTP状态码,错误码,对用户的提示信息。为了简化操作,这里封装了几个工具函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
go复制代码package common

import (
"github.com/gin-gonic/gin"
"net/http"
)

// Rsp 响应
type Rsp struct {
Code string `json:"Code,omitempty"` // 错误码
Msg string `json:"Msg,omitempty"` // 消息
Data interface{} `json:"Data,omitempty"` // 数据
}

// SuccessRsp 成功响应
func SuccessRsp(data interface{}) *Rsp {
return rsp(ErrCodeOK, data)
}

// FailureRsp 失败响应
func FailureRsp(err error) *Rsp {
errCode, ok := err.(*ErrCode)
if !ok {
errCode = ErrCodeInternalError
}
return rsp(errCode, nil)
}

// Success 请求成功
func Success(c *gin.Context, data interface{}) {
ginRsp(c, SuccessRsp(data))
}

// Failure 请求失败
func Failure(c *gin.Context, err error) {
ginRsp(c, FailureRsp(err))
}

// gin响应
func ginRsp(c *gin.Context, rsp *Rsp) {
c.JSON(http.StatusOK, rsp)
}

// 响应
func rsp(errCode *ErrCode, data interface{}) *Rsp {
return &Rsp{
Code: errCode.Code,
Msg: errCode.Advice,
Data: data,
}
}

Rsp的Msg使用了advice,因为这是给用户看的。

Gin HandlerFunc包装

对于一个服务,在使用Gin框架时,如果我们想把它暴露出去,需要编写一个HandlerFunc函数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码func LoginHandler(c *gin.Context) {
// 参数绑定
var req LoginReq
if err := c.ShouldBindJSON(&req); err != nil {
result.Failure(c, common.ErrCodeInvalidParameter)
return
}

// 调用服务
service := NewService()
rsp, err := service.Login(&req)

// 结果处理
if err != nil {
common.Failure(c, err)
return
}
common.Success(c, rsp)
}

但是由于我们的服务、请求、响应、错误码、状态码结构都是统一的,对于不同服务的handler,代码除了在请求参数类型不同外,其余都是一样的,这会导致handler的代码非常的冗余,因此我们利用反射机制对Service进行简单的包装,消除对handler的编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
go复制代码package wrap

import (
"github.com/gin-gonic/gin"
"github.com/jiaxwu/go_service_req_rsp_errcode_demo/service/common"
"reflect"
)

func WrapService(service interface{}) func(*gin.Context) {
return func(c *gin.Context) {
// 参数绑定
s := reflect.TypeOf(service)
reqPointType := s.In(0)
reqStructType := reqPointType.Elem()
req := reflect.New(reqStructType)
if err := c.ShouldBindJSON(req.Interface()); err != nil {
common.Failure(c, common.ErrCodeInvalidParameter)
return
}

// 调用服务
params := []reflect.Value{reflect.ValueOf(req.Interface())}
rets := reflect.ValueOf(service).Call(params)

// 结果处理
if !rets[1].IsNil() {
common.Failure(c, rets[1].Interface().(*common.ErrCode))
return
}
common.Success(c, rets[0].Interface())
}
}

这样,我们对外暴露服务只需要一行代码:

1
2
3
4
5
6
7
8
9
10
11
go复制代码package login

import (
"github.com/gin-gonic/gin"
"github.com/jiaxwu/go_service_req_rsp_errcode_demo/service/wrap"
)

// RegisterHandler 注册handler
func RegisterHandler(engine *gin.Engine, service *Service) {
engine.POST("/login", wrap.WrapService(service.Login))
}

还可以对wrapper进行扩展, 使其支持进行身份认证,选择是否有返回值(加入一个Config参数),自动注入multipart.Form,header等(使用Go的.(类型)操作对参数类型进行匹配)。

全部代码

Github:github.com/jiaxwu/go_s…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%