目录

让基于gin的HTTP应用开发更简单

在go语言中,开发http应用一般离不开gin、beego等web框架,使用这些框架的时候一般都会提供一些方法让你把请求数据绑定到数据结构体中,但是对于每个请求都要我们手动绑定一次未免太麻烦了。

个人感觉protobuf那种方式就挺不错的,定义了一些proto接口,然后生成代码,只需要实现方法就行,不用自己手动绑定数据,按照这个思路,我们在gin的http应用开发中也可以这样做。

1、设计需求

  • 定义一个结构体,该结构体内所有公开的方法都能自动被POST路由
  • 提供一个手动路由的方法,有时候我们需要提供一些GET请求以供资源的下载
  • 能被路由的方法要符合:func(ctx *gin.Context,req Request) (*Response,error) 格式,由框架统一封装返回的格式
  • 提供自定义返回格式的方法,能让使用者可以自定义返回格式
个人想法
个人认为,restfulAPI的设计方式已经过时了,比起规规矩矩地遵循restful风格,还不如全部梭哈成POST,这样有个好处就是程序员开发时心智损耗会大大降低,只需设计好url路径,就可以获取比restful更加清楚的效果。

于是,基于以上需求实现出来一个简易的框架。

2、使用方法

安装

go get github.com/MikeLINGxZ/simple-server-runner

定义http结构体

type Server struct {
}

type GetUserRequest struct {
	UserName string `json:"user_name"`
}

type GetUserResponse struct {
	Msg string `json:"msg"`
}

func (s *Server) GetUser(ctx *gin.Context, req GetUserRequest) (*GetUserResponse, error) {
	return &GetUserResponse{Msg: "hello,im " + req.UserName}, nil
}

初始化并运行

runner := simple_server_runner.NewDefaultServerRunner(&Server{})
runner.Run()

3、实现

3.1、定义框架结构体

首先我们需要一个框架用的结构体,结构体中包含一下内容:

  1. gin *gin.Engine:基于gin的框架
  2. server interface:这个用于定义我们的服务的结构体
  3. routerWhiteList map[string]struct{}:路由白名单,当我们手动绑定了路由后,自动路由就不必再绑定这个方法了,所以我们可以用一个map存起来
  4. responseFunc func(ctx *gin.Context, data interface{}, err error):返回数据的回调函数,默认情况下使用自带的方法返回数据,用户可以自定义返回方法

总结下来定义了如下一个结构体:

type ServerRunner struct {
	gin             *gin.Engine
	server          interface{}
	routerWhiteList map[string]struct{}
	responseFunc    func(ctx *gin.Context, data interface{}, err error)
}

3.2 关键方法实现

i. 自动路由绑定

首先明确一点,ServerRunner结构体中的server才是我们的http服务结构体,我们需要拿到这个周倜的所有公开方法进行自动路由。然后再遍历这个方法列表,检查参数,添加路由

func (s *ServerRunner) autoBindRouter() error {
    // 反射获取结构体
    serverTypeOf := reflect.TypeOf(s.server)
    // 获取方法数量
    methodNum := serverTypeOf.NumMethod()
    for i := 0; i < methodNum; i++ {
        // 获取方法
        method := serverTypeOf.Method(i)
        // 排除私有方法和Run方法
        _, ok := s.routerWhiteList[method.Name]
        if !method.IsExported() || ok {
            continue
        }
        methodFundType := method.Func.Type()
        
        ...
        检查入参格式
        检查出参格式
        ...
    
        // 创建 Gin 路由处理函数
        handlerFunc := func(c *gin.Context) {
            // 创建参数值的切片
            paramValues := make([]reflect.Value, 3)
            paramValues[0] = reflect.ValueOf(s.server).Elem().Addr()
            paramValues[1] = reflect.ValueOf(c).Elem().Addr()
            paramValues[2] = reflect.New(methodFundType.In(2)).Elem()
            
            // 绑定请求参数到结构体
            if c.Request.ContentLength > 0 {
                if err := c.ShouldBind(paramValues[2].Addr().Interface()); err != nil {
                    s.responseFunc(c, nil, err)
                    return
                }
            }
            // 绑定uri参数
            if err := c.ShouldBindQuery(paramValues[2].Addr().Interface()); err != nil {
                s.responseFunc(c, nil, err)
                return
            }
            
            // toto 调用函数
            returnValues := method.Func.Call(paramValues)
            
            // 处理返回值
            var resultValue interface{}
            if returnValues[0].IsValid() && returnValues[0].Kind() == reflect.Ptr && returnValues[0].Elem().IsValid() {
                resultValue = returnValues[0].Elem().Interface()
            } else if returnValues[0].IsValid() && returnValues[0].Kind() == reflect.Slice {
                resultValue = returnValues[0].Interface()
            }
            
            errValue, _ := returnValues[1].Interface().(error)
            s.responseFunc(c, resultValue, errValue)
        }
        // 添加路由
        s.gin.Handle("POST", method.Name, handlerFunc)
    }
}	

ii. 手动路由绑定

手动绑定路由其实和自动绑定差不多,只不过入参不同且多了加入白名单这个步骤

func (s *ServerRunner) BindRouter(method, path string, f interface{}) {
    // 检查f是否为一个可调用的函数
    funcType := reflect.TypeOf(f)
    if funcType.Kind() != reflect.Func {
        panic("router bind must be a func")
    }
	
	...
	
	functionName := s.getFunctionName(f)
    s.routerWhiteList[functionName] = struct{}{}
    // 添加路由
    s.gin.Handle(method, path, handlerFunc)
}

iii. 返回

默认返回如下,用户可以通过 CustomResponse 函数定义自己的返回。

func (s *ServerRunner) defaultResponse(ctx *gin.Context, data interface{}, err error) {
	commonResponse := CommonResponse{}
	if err != nil {
		commonResponse.Code = 500
		commonResponse.Msg = err.Error()
	} else {
		commonResponse.Code = 200
		commonResponse.Data = data
	}

	ctx.JSON(http.StatusOK, commonResponse)
}

自定义返回格式:

runner := simple_server_runner.NewDefaultServerRunner(&Server{})
runner.CustomResponse(func(ctx *gin.Context, data interface{}, err error) {
    resp := CustomResponse{}
    if err != nil {
        resp.Error = err.Error()
        ctx.JSON(http.StatusBadGateway, resp)
        return
    }
    resp.Data = data
    ctx.JSON(http.StatusOK, resp)
    return
})

iv. 运行

运行函数的实现就很简单了,代码如下:

func (s *ServerRunner) Run(addr ...string) error {
	err := s.autoBindRouter()
	if err != nil {
		return err
	}
	return s.gin.Run(addr...)
}

其他使用方法

手动绑定

  1. 定义需要路由的方法
type GetAgeRequest struct {
	UserName *string `form:"user_name"`
}

type GetAgeResponse struct {
	Msg string `json:"msg"`
}

func GetAge(ctx *gin.Context, req GetAgeRequest) (*GetAgeResponse, error) {
	if req.UserName == nil || *req.UserName == "" {
		return nil, errors.New("user name is nil")
	}
	return &GetAgeResponse{Msg: fmt.Sprintf("%s is 20 years old", req.UserName)}, nil
}
  1. 绑定
runner.BindRouter("GET", "GetAge", GetAge)

自定义返回

runner.CustomResponse(func(ctx *gin.Context, data interface{}, err error) {
    resp := CustomResponse{}
    if err != nil {
        resp.Error = err.Error()
        ctx.JSON(http.StatusBadGateway, resp)
        return
    }
    resp.Data = data
    ctx.JSON(http.StatusOK, resp)
    return
})