go-zero微服务框架代码生成神器goctl原理分析(一)

推荐下go-zero 微服务框架,也是最近很火很有人气的框架,致力于打造国内最简单好用的框架。

火在哪?几分钟搞定个接口和微服务,还不用担心稳定性和高并发,这不香吗?

作者的理念很好,工具大于约定和文档的理念编码自动化让精力更多的放在业务和创新上,大幅度的提高效率和生产力,这会是个趋势。

golang圈子不大,微服务框架框架屈指可数:除了go-micro、go-kit,几乎没有其他选择。go-zero为此提供第三个可能。

说起go-zero提高生产力的地方,就不得不说goctl。

goctl是go-zero配套的代码生成器。

简直是神器,只需要定义好服务接口和请求体,应答体,一键自动生成整套框架和代码。
对goctl是如何生成模板框架和代码感觉很好奇,于是就打算对goctl的实现做个分析。或许哪天也想搞个这么个代码生成器,可以参考下go-zero作者的思路和方法,也来提高提高效率,自动生成框架或模板代码多好。

分析之前先来看看goctl是怎么用的吧。

首先得先安装goctl了,

git clone https://github.com/tal-tech/go-zero
cd go-zero/tools/goctl
go build goctl.go

最后生成goctl.exe 复制到Go的安装目录bin下 。

简单的使用方法:

goctl api    go    -api       hello.api   -dir    .
#代码说明如下
goctl  api      go       -api             open.api            -dir                     .
 |      |        |         |                 |                  |                      | 
      生成api  go语言     指定api模板文件   模板文件名称         指定生成代码存放路径     当前文件夹

创建项目

生成go.mod文件

输入如下指令,创建项目

  1. mkdir hello

  2. cd hello

  3. go mod init hello

定义hello.api

type (
	UserOptReq struct {
		mobile string `json:"mobile" form:"mobile"`
		passwd string `json:"passwd" form:"passwd"`
		code   string `json:"code" form:"code"`
	}

	UserOptResp struct {
		id    uint   `json:"id"`
		token string `json:"token"`
	}
	//图片验证码支持
	VerifyReq struct {
		ticket string `json:"ticket" form:"ticket"`
	}
	//图片验证码支持
	VerifyResp struct {
		data string `json:"data"`
	}
)

service open-api {
	@doc(
        summary: 公开的api函数
        desc: >
        register 用户注册,
        authorization 用户登录,
        verify 图片验证码接口
    )
	@server(
		handler: registerHandler
		folder: open
	)
	post /open/register(UserOptReq) returns(UserOptResp)
	
	
	@server(
		handler: authorizationHandler
		folder: open
	)
	post /open/authorization(UserOptReq) returns(UserOptResp)

	@server(
		handler: verifyHandler
		folder: open
	)
	post /open/verify(VerifyReq) returns(VerifyResp)
	
}

生成代码

采用如下指令生成代码:

goctl api    go    -api       hello.api   -dir    .

 

运行一下

go run open.go

测试一下:

curl http://127.0.0.1:8888/open/register -X POST -H "Content-Type: application/json" -d {\"mobile\":\"15367151352\",\"passwd\":\"testpwd\",\"code\":\"asdf\"}
{"id":0,"token":""}

 熟练的话几分钟时间就搞定一个接口的大致框架剩下就剩添加业务了,这效率,还能更快吗?简单的接口分钟搞定。

接下来揭开goctl的神秘面纱,看下是如何实现这么强大的功能的,从中汲取点儿营养。跟着上面的指令流程,goctl api    go    -api       hello.api   -dir    .走一遍吧,看看经历了什么。

打开源码目录里,进入github.com\tal-tech\go-zero\tools\goctl,

打开goctl.go,这里应该是应用的入口了。goctl.go这个文件的源码不多,使用了一个外部库github.com/urfave/cli

先来看下这个cli库,这个库使用还是挺简单的,用来生成命令行应用的神器。

简单的命令行,直接用go内置的flag包实现了命令行参数的解析就可以了。但若是造成一个工具,得方便使用吧,有一些命令行的使用说明吧,使用urfave/cli这个库就简单了。

一个简单示例:

package main

import (
	"fmt"
	"github.com/urfave/cli"
	"os"
)

var (
	Version = "1.0.0"
)

type Commands struct {
}

func (this *Commands) Test(cli *cli.Context) {
	fmt.Println("this is Test cmd proc")
	uid := cli.Int("uid")
	username := cli.String("name")
	fmt.Println(uid, username)
}

//模拟子命令
func (this *Commands) Test1(cli *cli.Context) {
	fmt.Println("this is Test1 cmd proc")
	u := cli.Int("u")
	n := cli.String("n")
	fmt.Println(u, n)
}

func main() {
	app := cli.NewApp()
	app.Usage = "this is a cli tool to do someting"
	app.Version = Version
	app.Commands = []cli.Command{
		{
			Name:   "test",
			Usage:  "this is a test cmd,for example:test -uid=x -name=y",
			Action: (&Commands{}).Test,
			Flags: []cli.Flag{
				cli.IntFlag{Name: "uid,ud", Usage: "this is a flag,-uid"},
				cli.StringFlag{Name: "name,ne", Usage: "this is a flag,-name"},
			},
			//以下子命令,根据需要有,非必须
			Subcommands: []cli.Command{
				{
					Name:   "test1",
					Usage:  "this is a sub cmd test1",
					Action: (&Commands{}).Test1,
					Flags: []cli.Flag{
						cli.IntFlag{Name: "u", Usage: "this is a flag,n"},
						cli.StringFlag{Name: "n", Usage: "this is a flag,usage:-n"},
					},
				},
			},
		},
	}
	err := app.Run(os.Args)
	if err != nil {
		fmt.Print("command error :" + err.Error())
	}
}

或者这样,注意参数输入可以为-u,或者--u都行,或者-u 后面跟等号或者省略=号直接写内容都可以。

了解完cli内容,那么可知上面的goctl命令的执行顺序了,goctl api    go    -api       hello.api   -dir    .

就是执行了api命令的子命令go的那项,参数输入是-api,内容为hello.api,另一项参数输入为-dir,内容为当前目录。

Action: gogen.GoCommand,

接下来就进入gogen.GoCommand里面看看吧,代码在github.com\tal-tech\go-zero\tools\goctl\api\gogen的gen.go里面。

const tmpFile = "%s-%d"

var tmpDir = path.Join(os.TempDir(), "goctl")

func GoCommand(c *cli.Context) error {
	apiFile := c.String("api")
	dir := c.String("dir")
	if len(apiFile) == 0 {
		return errors.New("missing -api")
	}
	if len(dir) == 0 {
		return errors.New("missing -dir")
	}

	p, err := parser.NewParser(apiFile)
	if err != nil {
		return err
	}
	api, err := p.Parse()
	if err != nil {
		return err
	}

	logx.Must(util.MkdirIfNotExist(dir))
	logx.Must(genEtc(dir, api))
	logx.Must(genConfig(dir))
	logx.Must(genMain(dir, api))
	logx.Must(genServiceContext(dir, api))
	logx.Must(genTypes(dir, api))
	logx.Must(genHandlers(dir, api))
	logx.Must(genRoutes(dir, api))
	logx.Must(genLogic(dir, api))
	// it does not work
	format(dir)
	createGoModFileIfNeed(dir)

	if err := backupAndSweep(apiFile); err != nil {
		return err
	}

	if err = apiformat.ApiFormat(apiFile, false); err != nil {
		return err
	}

	fmt.Println(aurora.Green("Done."))
	return nil
}

从这段代码里,可以看到,从命令行取出了hello.api这个文件,先经过parser.NewParser(apiFile)这一处关键处理,这应是goctl的神秘地方之一,后面会单独分析。

然后依次是如果目录不存在,创建目录,logx.Must用意就是must里面的必须执行成功,不成功的话就中断退出了,并输出日志。

接下来流程依次是

genEtc生成etc配置文件目录和内容。

genConfig-> genMain,genServiceContext,genTypes,genHandlers,genRoutes,genLogic

然后执行go fmt指令格式化一下代码format(dir)

func format(dir string) {
	cmd := exec.Command("go", "fmt", "./"+dir+"...")
	_, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Println(err.Error())
	}
}

createGoModFileIfNeed 创建下go.mod如果需要的话。

backupAndSweep,备份和清理处理。

最后是apiformat.ApiFormat(apiFile, false)是干嘛的,先记下来,后面再看。既然是分析原理嘛,起初没必要每个点都扣那么细,讲究方法,先整体后局部,先易后难。

以上仅仅是冰山一角,一个大致的流程。

难点和精华应该是那个parser.NewParser()处理和后面genEtc和genMain等里面的text/template模板解析和作者实现的获取注解的方法实现上。

下面接着分析parser,这块儿是重点,涉及go/parser和go/ast库的使用。

未完待续集......

猜你喜欢

转载自blog.csdn.net/qq8864/article/details/114656942