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

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

状态: 未更新

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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
1
2
3
4
5
# ./cobra-demo6

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

PersistentPreRun in sub1
Flags:
  -c, --config string   配置文件 (default "$HOME/.config.json")
  1. ./cobra06-demo sub2
1
2
3
4
5
6
7
# ./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
1
2
3
4
5
6
7
# ./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 的结构体

1
2
3
4
5
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 字段都会被修改。

1
2
3
4
5
6
7
8
9
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 开始。

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

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