当前位置: 代码网 > it编程>前端脚本>Golang > mangokit:golang web项目管理工具,使用proto定义http路由和错误

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

2024年08月03日 Golang 我要评论
mangokit是一个web项目的管理工具,它的功能如下:1. 根据预设的项目结构创建出一个web项目,使用已有的代码框架,减少工作量2. 使用proto来定义http路由以及错误码,使用相关工具生成代码,完成自动结构体反序列化以及返回值响应3. 使用wire来管理依赖注入,减少依赖管理的烦恼

前言

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

func handler(ctx *gin.context) {
    user := new(user)
    if err := ctx.shouldbind(user); err != nil {
        ...
    }
    
    ...
    resp := serivce()
    ...
    
    ctx.json(http.statusok, resp)
}

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

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

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 {
    ...
}
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

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框架对应的路由处理器:

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的代码如下:

// 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

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代码:

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中的代码如下:

// 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: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即可自动生成依赖创建的代码:

// 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依赖注入代码

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文件中的信息,比如:proto中定义的message、service、enum等。
在这里插入图片描述

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

首先看main函数:

protogen.options.run来运行我们的程序

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

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

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

mangokit create mangokit-test mangokit_test

在这里插入图片描述

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

项目目录结构如下:

$ 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:

mangokit create -r https://gitee.com/mangohow/mangokit-template mangokit-test mangokit_test

3.2 添加新的proto文件

在这里插入图片描述

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

# 添加http api
mangokit add api api/helloworld/v1/proto hello.proto

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

syntax = "proto3";

package hello.v1;

import "google/api/annotations.proto";

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

service hello {

}

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

mangokit add error api/errors/v1/proto errorreason.proto

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

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生成代码

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

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

mangokit generate wire

生成openapi文档

mangokit generate openapi

生成上面所有的三个项目

mangokit generate all
(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com