mangokit:golang web项目管理工具,使用proto定义http路由和错误

文章目录

    • 前言
    • 1、mangokit介绍
      • [1.1 根据proto文件生成http路由](#1.1 根据proto文件生成http路由)
      • [1.2 根据proto文件生成响应码](#1.2 根据proto文件生成响应码)
      • [1.3 使用wire来管理依赖注入](#1.3 使用wire来管理依赖注入)
    • 2、mangokit实现
      • [2.1 protobuf插件开发](#2.1 protobuf插件开发)
      • [2.2 mangokit工具](#2.2 mangokit工具)
    • 3、使用示例
      • [3.1 创建新项目](#3.1 创建新项目)
      • [3.2 添加新的proto文件](#3.2 添加新的proto文件)
      • [3.3 代码生成](#3.3 代码生成)

前言

在使用gin框架开发web应用时,需要我们自己手动完成请求到结构体的反序列化,以及发送响应,如下:

go 复制代码
func Handler(ctx *gin.Context) {
    user := new(User)
    if err := ctx.ShouldBind(user); err != nil {
        ...
    }
    
    ...
    resp := serivce()
    ...
    
    ctx.Json(http.StatusOk, resp)
}

显然,这些工作都是多余的,和业务无关的,每个handler都需要我们自己处理,非常的麻烦

为了解决这个问题,我们可以使用反射的方式来字段完成请求数据到结构体的映射;对于响应,则定义一个统一的结构体,并且让handler返回这个结构体,如下:

go 复制代码
type Response struct {
	R      RespValue
	Status int
}

type RespValue struct {
	Data interface{} `json:"data"`
    Codee    int    `json:"code"`
	Messagee string `json:"message"`
}

func NewResponse(status, code int, message string, data interface{}) *Response {
    ...
}
go 复制代码
func Handler(ctx *gin.Context, user *User) *Response {
    ...
    resp = service()
    ...
    
    return NewResponse(http.StatusOk, 0, "success", resp)
}

在注册路由时,则需要使用反射来对我们的handler进行适配,使用反射机制创建请求参数,然后将数据反序列化为对应的结构体,然后调用我们定义的handler,并且获取到返回值,调用ctx.Json来发送

这种方式方便了我们的开发,但是使用反射会对程序带来一定的性能损失(但是在这里只是简单的使用,性能损失很少),并且使用反射容易出现错误

最近在使用了bilibili的kratos框架后,给了我一些灵感,我们完全可以使用proto来定义http的路由,然后生成反序列化的结构代码,并且可以使用proto来定义返回错误码等。

因此借鉴了kratos的设计,我实现了一个小工具用来加速我的web开发

github:https://github.com/mangohow/mangokit

1、mangokit介绍

mangokit是一个web项目的管理工具,它的功能如下:

  1. 根据预设的项目结构创建出一个web项目,使用已有的代码框架,减少工作量
  2. 使用proto来定义http路由以及错误码,使用相关工具生成代码,完成自动结构体反序列化以及返回值响应
  3. 使用wire来管理依赖注入,减少依赖管理的烦恼

1.1 根据proto文件生成http路由

proto定义文件如下:

hello.proto

protobuf 复制代码
syntax = "proto3";

package hello.v1;

import "google/api/annotations.proto";

option go_package = "api/helloworld/v1;v1";

// 定义service
service Hello {
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      get: "/hello/:name"
    };
  }
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 2;
}

然后使用mangokit命令根据proto生成gin框架对应的路由处理器:

shell 复制代码
mangokit generate proto api

生成的文件如下:

hello.pb.go

hello_http_gin.pb.go

其中hello.pb.go是protoc --go-out生成的,而hello_http_gin.pb.go是我们自己写的proto插件protoc-gen-go-gin生成的

hello_http_gin.pb.go的代码如下:

go 复制代码
// Code generated by protoc-gen-go-gin. DO NOT EDIT.
// versions:
// - protoc-gen-go-gin v1.0.0
// - protoc             v3.20.1
// source: helloworld/v1/proto/hello.proto

package v1

import (
	"context"
	"net/http"

	"github.com/mangohow/mangokit/serialize"
	"github.com/mangohow/mangokit/transport/httpwrapper"
)

type HelloHTTPService interface {
	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

func RegisterHelloHTTPService(server *httpwrapper.Server, svc HelloHTTPService) {
	server.GET("/hello/:name", _Hello_SayHello_HTTP_Handler(svc))
}

func _Hello_SayHello_HTTP_Handler(svc HelloHTTPService) httpwrapper.HandlerFunc {
	return func(ctx *httpwrapper.Context) error {
		in := new(HelloRequest)
		if err := ctx.BindRequest(in); err != nil {
			return err
		}

		value := context.WithValue(context.Background(), "gin-ctx", ctx)
		reply, err := svc.SayHello(value, in)
		if err != nil {
			return err
		}

		ctx.JSON(http.StatusOK, serialize.Response{Data: reply})

		return nil
	}
}

在上面生成的go代码中,包含一个接口的定义,其中包含了我们定义的handler方法

并且提供了RegisterHelloHTTPService函数来注册路由,注册的路由为_Hello_SayHello_HTTP_Handler函数,在这个函数中有反序列化的代码,以及响应代码

因此我们只需要实现HelloHTTPService中的方法,并且调用RegisterHelloHTTPService来注册路由即可,大大的减少了我们的工作量。

这有点类似于grpc的方式。

1.2 根据proto文件生成响应码

有时候只使用http的状态码是不够的,比如200表示请求成功,但是虽然请求成功了,还可能出现其它问题。

比如一个登录的接口,用户登录时可能出现以下的情况:1、用户不存在 2、密码错误 3、用户被封禁了

因此,我们需要定义相关的一些响应码来处理这些情况

proto定义文件如下:

errors.proto

protobuf 复制代码
syntax = "proto3";

package errors.v1;
import "errors/errors.proto";

option go_package = "api/errors/v1;v1";

enum ErrorReason {
  // 设置缺省错误码
  option (errors.default_code) = 500;

  Success = 0 [(errors.code) = 200];

  // 为某个枚举单独设置错误码
  UserNotFound = 1 [(errors.code) = 200];

  UserPasswordIncorrect = 2 [(errors.code) = 200];

  UserBanned = 3 [(errors.code) = 200];
}

在上面的proto文件中,我们使用enum来定义响应码,其中包括int类型的响应码,以及返回的http状态码(errors.code)

然后使用mangokit来生成go代码:

shell 复制代码
mangokit generate proto api

生成的文件如下:

errors.pb.go

errors_errors.pb.go

其中errors.pb.go是protoc --go_out生成的,而errors_errors.pb.go同样也是自己编写的proto插件protoc-gen-go-error生成的

errors_errors.pb.go中的代码如下:

go 复制代码
// Code generated by protoc-gen-go-error. DO NOT EDIT.
// versions:
// - protoc-gen-go-error v1.0.0
// - protoc              v3.20.1
// source: errors/v1/proto/errors.proto

package v1

import (
	"fmt"

	"github.com/mangohow/mangokit/errors"
)

func ErrorSuccess(format string, args ...interface{}) errors.Error {
	return errors.New(int32(ErrorReason_Success), 200, ErrorReason_Success.String(), fmt.Sprintf(format, args...))
}

// 为某个枚举单独设置错误码
func ErrorUserNotFound(format string, args ...interface{}) errors.Error {
	return errors.New(int32(ErrorReason_UserNotFound), 200, ErrorReason_UserNotFound.String(), fmt.Sprintf(format, args...))
}

func ErrorUserPasswordIncorrect(format string, args ...interface{}) errors.Error {
	return errors.New(int32(ErrorReason_UserPasswordIncorrect), 200, ErrorReason_UserPasswordIncorrect.String(), fmt.Sprintf(format, args...))
}

func ErrorUserBanned(format string, args ...interface{}) errors.Error {
	return errors.New(int32(ErrorReason_UserBanned), 200, ErrorReason_UserBanned.String(), fmt.Sprintf(format, args...))
}

然后我们就可以调用这些函数来生成具体的响应码,减少我们的代码工作量

1.3 使用wire来管理依赖注入

wire是谷歌开源的一款依赖注入工具,相比于其它的反射式的依赖注入方式,wire采用代码生成的方式来完成依赖注入,代码运行效率更高

代码如下:

go 复制代码
//go:generate wire
//go:build wireinject
// +build wireinject

package main

import (
	"github.com/google/wire"
	"mangokit_test/internal/conf"
	"mangokit_test/internal/dao"
	"mangokit_test/internal/server"
	"mangokit_test/internal/service"
	"github.com/mangohow/mangokit/transport/httpwrapper"
	"github.com/sirupsen/logrus"
)

func newApp(serverConf *conf.Server, dataConf *conf.Data, logger *logrus.Logger) (*httpwrapper.Server, func(), error) {
	panic(wire.Build(dao.ProviderSet, service.ProviderSet, server.NewHttpServer))
}

根据上面的代码,wire即可自动生成依赖创建的代码:

go 复制代码
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
	"mangokit_test/internal/conf"
	"mangokit_test/internal/dao"
	"mangokit_test/internal/server"
	"mangokit_test/internal/service"
	"github.com/mangohow/mangokit/transport/httpwrapper"
	"github.com/sirupsen/logrus"
)

// Injectors from wire.go:

func newApp(serverConf *conf.Server, dataConf *conf.Data, logger *logrus.Logger) (*httpwrapper.Server, func(), error) {
	db, cleanup, err := dao.NewFakeMysqlClient(dataConf)
	if err != nil {
		return nil, nil, err
	}
	greeterDao := dao.NewGreeterDao(db)
	greeterService := service.NewGreeterService(greeterDao, logger)
	httpwrapperServer := server.NewHttpServer(serverConf, logger, greeterService)
	return httpwrapperServer, func() {
		cleanup()
	}, nil
}

同样的mangokit中也添加了相应的指令来生成wire依赖注入代码

shell 复制代码
mangokit generate wire

2、mangokit实现

mangokit主要包含三个组件:

  • protoc-gen-go-gin
  • protoc-gen-go-error
  • mangokit

protoc-gen-go-gin用于根据proto文件中定义的service来生成gin框架的路由代码

protoc-gen-go-error用于根据proto文件中定义的enum来生成相应的响应错误码

mangokit中则设置了多种指令用于管理项目,比如:

  1. 使用create命令来生成一个初始项目结构
  2. 使用add命令来添加proto文件、makefile或Dockerfile
  3. 使用generate命令来根据proto文件生成go代码、生成openapi以及生成wire依赖注入

2.1 protobuf插件开发

在使用protoc时可以指定其它的插件用于生成代码,比如:

  • --go_out则会调用protoc-gen-go插件来生成go的代码
  • --go-grpc_out则会调用protoc-gen-go-grpc插件来生成grpc的代码

同样的,我们可以使用go来实现一个类似的插件,从而根据proto文件来生成gin框架的代码以及响应码代码

工作原理:

在使用 protoc --go-gin_out时,protoc会解析proto文件,然后生成抽象语法树,并且它会使用protobuf语法树序列化为二进制序列,然后使用标准输入将二进制序列传入我们的插件中,然后再使用protobuf进行反序列化,然后我们在自己的程序中就可以根据提供的信息来生成go代码,比如:proto中定义的message、service、enum

开发proto插件我们可以使用google.golang.org/protobuf/compiler/protogen

我们可以参考kratos的代码来实现自己的代码:https://github.com/go-kratos/kratos/tree/main/cmd/protoc-gen-go-errors

首先看main函数:

protogen.Options.Run来运行我们的程序

在传入的匿名函数中,我们会接收到protogen.Plugin参数,该参数中有proto文件中定义的各种结构的详细信息

然后我们可以遍历每个proto文件来生成相应的代码,在generateFile中生成代码

go 复制代码
package main

import (
	"flag"
	"fmt"

	"google.golang.org/protobuf/compiler/protogen"
	"google.golang.org/protobuf/types/pluginpb"
)

var (
	showVersion = flag.Bool("version", false, "print the version and exit")
)


func main() {
	flag.Parse()
	if *showVersion {
		fmt.Printf("protoc-gen-go-gin %v\n", version)
		return
	}

	protogen.Options{
		ParamFunc: flag.CommandLine.Set,
	}.Run(func(plugin *protogen.Plugin) error {
		plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
		for _, f := range plugin.Files {
			if !f.Generate {
				continue
			}

			generateFile(plugin, f)
		}

		return nil
	})
}

在protogen.File中保存了一个proto文件中定义的各种结构解析后的信息:

详细代码参考:https://github.com/mangohow/mangokit

代码编写好之后编译为二进制程序,在使用protoc时指定插件名称,我们的插件一定要以protoc-gen开头,在指定插件名称时指定protoc-gen后面的部分拼接上_out即可;比如protoc-gen-go-error,在使用时为:protoc --go-error_out=. hello.proto

2.2 mangokit工具

mangokit使用cobra命令行工具开发,包含以下功能:

  1. 创建基础项目:根据预设的项目目录结构和代码生成
  2. 添加文件:包括api proto、error proto、makefile和Dockerfile
  3. 生成代码:包括go代码生成、wire生成和openapi生成

3、使用示例

3.1 创建新项目

首先使用mangokit来创建一个项目,项目目录为mangokit-test,go mod名称为mangokit_test

go 复制代码
mangokit create mangokit-test mangokit_test

然后执行cd mangokit-test && go mod tidy来下载依赖

项目目录结构如下:

shell 复制代码
$ tree
.
|-- api
|   |-- errors
|   |   `-- v1
|   |       |-- errors.pb.go
|   |       |-- errors_errors.pb.go
|   |       `-- proto
|   |           `-- errors.proto
|   `-- helloworld
|       `-- v1
|           |-- greeter.pb.go
|           |-- greeter_http_gin.pb.go
|           `-- proto
|               `-- greeter.proto
|-- cmd
|   `-- server
|       |-- main.go
|       |-- wire.go
|       `-- wire_gen.go
|-- configs
|   `-- application.yaml
|-- internal
|   |-- conf
|   |   |-- conf.pb.go
|   |   `-- conf.proto
|   |-- dao
|   |   |-- dao.go
|   |   |-- data.go
|   |   `-- userdao.go
|   |-- middleware
|   |-- model
|   |   `-- user.go
|   |-- server
|   |   `-- http.go
|   `-- service
|       |-- helloservice.go
|       `-- service.go
|-- pkg
|-- test
|-- third_party
|-- go.mod
|-- go.sum
|-- makefile
|-- Dockerfile
|-- openapi.yaml

32 directories, 52 files
  • api:api目录用来放置proto文件以及根据proto文件生成的go代码,通常将.proto文件放在proto文件夹下,而生成的代码放在它的上一级目录,这样看起来更清晰一些
  • cmd:cmd目录存放了wire注入代码和main文件
  • configs:configs目录用来放置程序的配置文件
  • internal:internal用来存放本项目依赖的代码,不会暴露给其它的项目,其中包括middleware(中间件)model(数据库结构体模型)、dao(数据库访问对象)、conf(配置信息代码)server(服务初始化代码)service(service的具体实现代码)
  • pkg:用来存放一些共用代码
  • test:存放测试代码
  • third_party:其中包含一些使用到的proto的扩展文件

在创建项目时默认会从github拉取一个预制的项目结构,如果遇到网络问题导致无法拉取,则可以使用-r命令来指定其它的仓库,比如使用gitee:

shell 复制代码
mangokit create -r https://gitee.com/mangohow/mangokit-template mangokit-test mangokit_test

3.2 添加新的proto文件

可以使用下面的命令来添加新的proto文件

shell 复制代码
# 添加http api
mangokit add api api/helloworld/v1/proto hello.proto

然后就会在api/helloworld/v1/proto目录下生成一个hello.proto文件

protobuf 复制代码
syntax = "proto3";

package hello.v1;

import "google/api/annotations.proto";

option go_package = "api/helloworld/v1;v1";

service Hello {

}

使用下面的命令来添加error proto

go 复制代码
mangokit add error api/errors/v1/proto errorReason.proto

同样的,在api/errors/v1/proto目录下生成了errorReason.proto文件

protobuf 复制代码
syntax = "proto3";

package errorReason.v1;

import "errors/errors.proto";

option go_package = "api/errors/v1;v1";

enum ErrorReason {
	option (errors.default_code) = 500;

	Placeholder = 0 [(errors.code) = 0];

}

除了添加proto文件,还可以添加预制的makefile和Dockerfile

3.3 代码生成

根据proto生成代码

shell 复制代码
# 根据api目录下的proto文件生成go代码
mangokit generate proto api

根据wire依赖注入生成代码:

shell 复制代码
mangokit generate wire

生成openapi文档

shell 复制代码
mangokit generate openapi

生成上面所有的三个项目

shell 复制代码
mangokit generate all
相关推荐
源代码•宸10 小时前
goframe框架签到系统项目(BITFIELD 命令详解、Redis Key 设计、goframe 框架教程、安装MySQL)
开发语言·数据库·经验分享·redis·后端·mysql·golang
王中阳Go14 小时前
Golang框架封神榜!GitHub星标TOP8大比拼,选对框架少走3年弯路
后端·面试·go
王中阳Go14 小时前
05 Go Eino AI应用开发实战 | Docker 部署指南
人工智能·后端·go
普通网友14 小时前
Bash语言的图算法
开发语言·后端·golang
雨岚霏15 小时前
Bash语言的数据库编程
开发语言·后端·golang
Way2top16 小时前
Go语言动手写Web框架 - Gee第三天 前缀树路由Router
go
王中阳Go16 小时前
06 Go Eino AI应用开发实战 | Eino 框架核心架构
人工智能·后端·go
bybitq17 小时前
Go函数闭包实战-复用函数
ios·golang·xcode
ChineHe19 小时前
Gin框架基础篇002_获取/绑定请求参数
后端·golang·gin
码界奇点21 小时前
基于Gin+Vue的前后端分离权限管理系统设计与实现
前端·vue.js·车载系统·毕业设计·gin·源代码管理