DevopsCamp 第 2 期作业: 《cobra - 05 Cobra 的子命令》 简单说下 cobra 命令树和 gin 路由树的实现差异

原文链接: https://typonotes.com/posts/2023/02/14/devopscamp-cobra-05-subcommand/

cobra 的子命令

在 cobra 中, 每个 命令 都是独立的。 通过 parent.AddCommand(children) 的形式进行串连。

1
2
3
4
5
6
7
var root = &cobra.Command{}

var child = &cobra.Command{}

func init() {
	root.AddCommand(child)
}

没了, 应用上就这么多。

cobra 命令树

如果你用过 gin 的路由树的话, 可能会对 cobra 的命令树实现更为深刻。

删除多余的结构, cobra 节点 就是下面这样的。

1
2
3
4
5
6
type Command struct {
	// 子命令
	commands []*Command
	// 父命令
	parent *Command
}

当进行 命令树 组合的时候, 是通过 领养/挂靠 的方式实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// AddCommand adds one or more commands to this parent command.
func (c *Command) AddCommand(cmds ...*Command) {
	for i, x := range cmds {
		if cmds[i] == c {
			panic("Command can't be a child of itself")
		}

		// 为子命令设置父命令(认亲)
		cmds[i].parent = c // (1)
// 省略
		// 添加传入的子命令
		c.commands = append(c.commands, x) // (2)
	}
}

(1) 在我们可以在任意节点调用 Execute 方法, 这个调用会通过 递归 找到最上层的 节点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (c *Command) Execute() error {
	_, err := c.ExecuteC()
	return err
}

// ExecuteC executes the command.
func (c *Command) ExecuteC() (cmd *Command, err error) {
	// Regardless of what command execute is called on, run on Root only
	// 递归寻找最上层
	if c.HasParent() {
		return c.Root().ExecuteC()
	}
}

再来看一次 Command 结构体定义

1
2
3
4
5
6
type Command struct {
	// 子命令
	commands []*Command
	// 父命令
	parent *Command
}

(2) 通过将传入的命令保存到 []*Command 切片中, 实现了命令树的 组合

1
c.commands = append(c.commands, x) // (2)

且同时 实现了 同一个命令挂载到多个父命令上

1
2
root.AddCommand(sub1, sub2) // (a)
sub1.AddCommand(sub2) // (b)

commands 是一个切片, 用于保存 挂载 上来的子命令。 但是, 这里需要注意, 由于 *Command 是指针类型, 意味着在任何修改, 都可能影响其他调用的地方。

对于 sub2 而言, (a)处父节点是 root(b)处父节点是 sub1最终父节点是 sub1。 但不影响 sub2 同时是 rootsub1 的子命令。

总体来说, cobra 命令节点 独立而又统一

gin 的路由树

与 cobra 相比, gin 的路由树实现就是另外一种方式了, 我称之为 生长。 换句话说, gin 路由的子节点不能独立于父节点 单独 定义。

1
2
3
4
5
6
7
8
9
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}

从上面代码可以看出来, RouterGroup 是通过 Group 方法实现路由节点 生长 的, 在调用 Group 方法的时候,

  1. 必须 要传入 子节点 的相对路径
  2. 使用私有方法计算出 basePath 的值。

这个特别像怀孕生孩子, 一代接一代。 绝对错不了。 O.o

gin 的 RouterGroup 能实现 认亲/挂靠 模式吗?

肯定可以, 做个变形手术就好了。 这里挖个坑吧。