《Shell 转 Go》
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

6. 持久化命令与Go引用类型的使用

DevOpsCamp第2期:从 《cobra - 06 持久化命令》 开始聊聊 Go语言 指针类型的使用注意事项

如果在 公众号 文章发现状态为 已更新, 建议点击 查看原文 查看最新内容。

状态: 未更新

原文链接: https://typonotes.com/posts/2023/02/19/devopscamp-cobra-06-persistent-run-and-flags/

嗯, 在 cobra 中提供了一种叫做 Persistent状态, 定向支持 函数参数

下面这段代码是是使用时的定义。

var root = &cobra.Command{
	Use: "root",

	// Persistent Run (1)
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		fmt.Println("PersistentPreRun in root")
	},
	Run: func(cmd *cobra.Command, args []string) {
		_ = cmd.Help()
	},
}

var config string

func init() {
	// Persistent Flag (2)
	root.PersistentFlags().StringVarP(&config, "config", "c", "~/.config.json", "配置文件")
}

凡是定义了 Persistent Run(1) 和 Flag(2) 的节点, 其子孙节点都会 继承 这种状态。 这种状态也可以被子孙节点的自定义状态覆盖。

注意: 这种状态是继承自 其父节点, 而非 上级节点

父节点不是上级节点

这个的问题的发生原因, 还是在 xxx.AddCommand 的时候造成的。 因为 &cobra.Command{} 是指针对象/引用对象, 因此在不同的地方修改是会全局影响的。 这个是基础知识, 也是重要的 点, 需要牢记。

下面是一个代码案例, 帮助理解。

案例代码在在 Github: https://github.com/tangx-labs/cobra06-demo

代码中实现了一个如下图所示的 命令树 结构。 其中 sub2 同时挂载到了 rootsub1 节点。

以下是代码执行结果执行结果, 注意看

  1. --config 的参数值
  2. PersistentPreRun 的执行结果
  3. sub2Usage 路径, 无论从哪里进入, 都是 root sub1 sub2

代码执行过程

  1. ./cobra-demo6
# ./cobra-demo6

PersistentPreRun in root
Flags:
  -c, --config string   配置文件 (default "~/.config.json")
  1. ./cobra-demo6 sub1
# ./cobra-demo6 sub1 

PersistentPreRun in sub1
Flags:
  -c, --config string   配置文件 (default "$HOME/.config.json")
  1. ./cobra06-demo sub2
# ./cobra06-demo sub2  # (1)

PersistentPreRun in sub1 # (2)
Usage:
  root sub1 sub2 [flags] # (3)
Global Flags:
  -c, --config string   配置文件 (default "$HOME/.config.json") # (4)
  1. ./cobra06-demo sub1 sub2
# ./cobra06-demo sub1 sub2 #(1)

PersistentPreRun in sub1 #(2)
Usage:
  root sub1 sub2 [flags] #(3)
Global Flags:
  -c, --config string   配置文件 (default "$HOME/.config.json") #(4)

引用类型回顾

首先, 我们再来回顾一下一下 Cobra Command 的结构体

type Command struct {
	Use string
	parent *Command	// 父命令
	commands []*Command	// 子命令
}

可以看到, parent *Command 是指针类型。 这意味着, 在任何地方修改都会影响全局引用

这里简单的回顾一下 引用对象A, B, C 都指向 指针地址, 而 指针地址 纸箱 真实数据地址。 当因为某个外力修改了 真实数据地址 中的内容的时候, 虽然 A, B, C 都没变化, 但是他们 到的东西发生了变化。

举个例子,

  1. 你去温泉之前寄存物品, 商家会给你一个 手牌(指针地址), 你拿着手牌将 名贵手表(数据) 放入对应的 100号柜子(真实地址)
  2. 在你泡温泉的时候, 某个人(外力) 打开了柜子, 把你的手表换成了 塑料手表(数据修改)
  3. 等你回来的时候, 虽然你的手牌没变, 但是你打开柜子的时候, 你拿到的不再你期望的东西了。

而正好, 每次执行 xxx.AddCommand(children) 添加子命令的时候, children 节点的 parent 字段都会被修改。

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  // child 的父节点会被修改
// 省略
	}
}

当一个节点, 重复被假如到其他节点的时候, 会出现这个问题。

cobra 树实现过程解析

下面, 我们对命令树的实现过程进行拆分。 注意 每个节点分为上下两层, 上层 表示父节点的名称, 下层 表示当前节点。

  parent
----------
   node

当执行第一条命令时时候, 此时创建的命令树就有所差异了。 左侧时从 root 开始, 右侧是从 sub1 开始。

当执行的第二条命令的时候, 都实现了相同结构的命令树(不看父节点差异的话)。

但仔细分析其内部节点, 可以知道, 相同位置的节点, 其父节点不一样