阅读(352) (13)

GoFrame 命令管理-结构化参数

2022-03-28 15:18:28 更新

命令行管理痛点

前面我们介绍的命令行管理,都是通过回调函数的​parser​对象获取解析的参数及选项数据,在使用的时候存在以下痛点:

  • 需要手动传入硬编码的参数索引或者选项名称信息来获取数据
  • 难以定义参数/选项的说明介绍
  • 难以定义参数/选项的数据类型
  • 难以对参数/选项进行数据校验
  • 对于需要管理大量命令行的项目是个灾难

对象化管理命令

我们来一个最简单的结构化管理参数示例。我们将前面介绍过的​Command​示例改造为结构化管理:

package main

import (
	"context"
	"fmt"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gcmd"
	"github.com/gogf/gf/v2/os/gctx"
)

type cMain struct {
	g.Meta `name:"main"`
}

type cMainHttpInput struct {
	g.Meta `name:"http" brief:"start http server"`
}
type cMainHttpOutput struct{}

type cMainGrpcInput struct {
	g.Meta `name:"grpc" brief:"start grpc server"`
}
type cMainGrpcOutput struct{}

func (c *cMain) Http(ctx context.Context, in cMainHttpInput) (out *cMainHttpOutput, err error) {
	fmt.Println("start http server")
	return
}

func (c *cMain) Grpc(ctx context.Context, in cMainGrpcInput) (out *cMainGrpcOutput, err error) {
	fmt.Println("start grpc server")
	return
}

func main() {
	cmd, err := gcmd.NewFromObject(cMain{})
	if err != nil {
		panic(err)
	}
	cmd.Run(gctx.New())
}

可以看到,我们通过对象的形式来管理父级命令,通过方法的形式来管理其下一层级的子级命令,并通过规范化的​Input​输入参数对象来定义子级命令的描述/参数/选项。大部分场景下,大家可以忽略​Output​返回对象的使用,但为规范化及扩展性需要保留,如果未用到,该返回参数直接返回​nil​即可。关于其中的结构体标签,后续会有介绍。

我们将示例代码编译后,执行查看效果:

$ main     
USAGE
    main COMMAND [OPTION]

COMMAND
    http    start http server
    grpc    start grpc server

DESCRIPTION
    this is the command entry for starting your process

使用​http​命令:

$ main http
start http server

使用​grpc​命令:

$ main grpc
start grpc server

效果和前面介绍的示例一致。

结构化管理入参

既然命令行通过对象化管理,我们仔细看看参数/选项是如何通过结构化管理。

我们将上面的实例简化一下,来个简单的例子,实现通过​http​命令开启​http​服务:

package main

import (
	"context"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gogf/gf/v2/os/gcmd"
	"github.com/gogf/gf/v2/os/gctx"
)

type cMain struct {
	g.Meta `name:"main" brief:"start http server"`
}

type cMainHttpInput struct {
	g.Meta `name:"http" brief:"start http server"`
	Name   string `v:"required" name:"NAME" arg:"true" brief:"server name"`
	Port   int    `v:"required" short:"p" name:"port"  brief:"port of http server"`
}
type cMainHttpOutput struct{}

func (c *cMain) Http(ctx context.Context, in cMainHttpInput) (out *cMainHttpOutput, err error) {
	s := g.Server(in.Name)
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Write("Hello world")
	})
	s.SetPort(in.Port)
	s.Run()
	return
}

func main() {
	cmd, err := gcmd.NewFromObject(cMain{})
	if err != nil {
		panic(err)
	}
	cmd.Run(gctx.New())
}

我们为​http​命令定义了两个输入参数:

  • NAME服务的名称,通过参数输入。这里使用了大写形式,方便展示在自动生成的帮助信息中
  • port服务的端口,通过​p/port​选项输入

并且我们通过​v:"required"​校验标签为这两个参数都绑定的必需的校验规则。是的,在​GoFrame​框架中,只要涉及到校验的地方都使用了统一的校验组件。

我们编译后执行看看效果:

$ main http
arguments validation failed for command "http": The Name field is required
1. arguments validation failed for command "http"
   1).  github.com/gogf/gf/v2/os/gcmd.newCommandFromMethod.func1
        /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_object.go:290
   2).  github.com/gogf/gf/v2/os/gcmd.(*Command).doRun
        /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_run.go:120
   3).  github.com/gogf/gf/v2/os/gcmd.(*Command).RunWithValueError
        /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_run.go:77
   4).  github.com/gogf/gf/v2/os/gcmd.(*Command).RunWithValue
        /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_run.go:32
   5).  github.com/gogf/gf/v2/os/gcmd.(*Command).Run
        /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcmd/gcmd_command_run.go:26
   6).  main.main
        /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.test/test.go:38
2. The Name field is required

执行后,报错了,这个错误来自于数据校验,表示必须参数(​Name/Port​)必须传递。

这里的报错打印了堆栈信息,因为​GoFrame​框架采用了全错误堆栈设计,所有组件错误都会带有自底向上的错误堆栈,以方便错误快速定位。当然我们可以通过​RunWithError​方法获取返回的错误对象关闭堆栈信息。

我们增加参数输入再试试:

$ main http my-http-server -p 8199
2022-01-19 22:52:45.808 [DEBU] openapi specification is disabled 

      SERVER     | DOMAIN  | ADDRESS | METHOD | ROUTE |                             HANDLER                             |    MIDDLEWARE      
-----------------|---------|---------|--------|-------|-----------------------------------------------------------------|--------------------
  my-http-server | default | :8199   | ALL    | /     | main.(*cMain).Http.func1                                        |                    
-----------------|---------|---------|--------|-------|-----------------------------------------------------------------|--------------------
  my-http-server | default | :8199   | ALL    | /*    | github.com/gogf/gf/v2/net/ghttp.internalMiddlewareServerTracing | GLOBAL MIDDLEWARE  
-----------------|---------|---------|--------|-------|-----------------------------------------------------------------|--------------------
 
2022-01-19 22:52:45.810 66292: http server started listening on [:8199] 

是的,这就对了。

完整使用案例

GoFrame​框架的开发工具大量使用了对象化、结构化的命令行管理,大家感兴趣可以更进一步查看源码了解:https://github.com/gogf/gf-cli

image2022-1-19_22-56-19

预定义的标签

在结构化设计中,我们使用了一些结构体标签,大部分来源于​Command​命令的属性,这里我们来介绍一下:

 标签  缩写  是否必须  说明  注意事项
 ​name  -  是  命名名称  
 ​short  -  -  命令缩写  
 ​usage  -  -  命令使用  
 ​brief  -  -  命令描述  
 ​age  -  -  表示该输入参数来源于参数而不是选项  仅用于属性标签
 ​orphan  -  -  表示该选项不带参数  属性通常为​bool​类型
 ​description  dc  -  命令的详细介绍  
 ​additional  ad  -  命令的额外描述信息  
 ​examples  eg  -  命令的使用示例  
 ​root  -  -  表示统一结构体方法中,该方法是父级命令,其他方法是它的子级命令  仅用于​Meta​标签
 ​strict  -  -  表示该命令严格解析参数/选项,当输入不支持的参数/选项时,返回错误  仅用于Meta标签
 ​config  -  -  表示该命令的选项数据支持从指定的配置读取,配置来源于默认的全局单例配置对象  仅用于​Meta​标签

高级特性

自动数据转换

结构化的参数输入支持自动的数据类型转换,您只需要定义好数据类型,其他的事情交给框架组件即可。自动数据类型转换出现在框架的很多组件中,特别是​HTTP/GRPC​服务的参数输入中。底层数据转换组件使用的是类型转换

自动数据校验

同样的,数据校验组件也是使用的统一的组件。

从配置读取数据

当命令行中没有传递对应的数据时,输入参数的结构体数据支持从配置组件中自动获取,只需要在​Meta​中设置​config​标签即可,配置来源于默认的全局单例配置对象。具体示例可以参考​GoFrame​框架开发工具源码:https://github.com/gogf/gf-cli