go语言部分源码研读| 青训营

48 阅读12分钟
func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) {
	if len(key) == 0 {
		if v, ok := n.(valueNode); ok {
			return !bytes.Equal(v, value.(valueNode)), value, nil
		}
		return true, value, nil
	}
	switch n := n.(type) {
	case *shortNode:
		matchlen := prefixLen(key, n.Key)
		// If the whole key matches, keep this short node as is
		// and only update the value.
		if matchlen == len(n.Key) {
			dirty, nn, err := t.insert(n.Val, append(prefix, key[:matchlen]...), key[matchlen:], value)
			if !dirty || err != nil {
				return false, n, err
			}
			return true, &shortNode{n.Key, nn, t.newFlag()}, nil
		}
		// Otherwise branch out at the index where they differ.
		branch := &fullNode{flags: t.newFlag()}
		var err error
		_, branch.Children[n.Key[matchlen]], err = t.insert(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val)
		if err != nil {
			return false, nil, err
		}
		_, branch.Children[key[matchlen]], err = t.insert(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value)
		if err != nil {
			return false, nil, err
		}
		// Replace this shortNode with the branch if it occurs at index 0.
		if matchlen == 0 {
			return true, branch, nil
		}
		// New branch node is created as a child of the original short node.
		// Track the newly inserted node in the tracer. The node identifier
		// passed is the path from the root node.
		t.tracer.onInsert(append(prefix, key[:matchlen]...))

		// Replace it with a short node leading up to the branch.
		return true, &shortNode{key[:matchlen], branch, t.newFlag()}, nil

	case *fullNode:
		dirty, nn, err := t.insert(n.Children[key[0]], append(prefix, key[0]), key[1:], value)
		if !dirty || err != nil {
			return false, n, err
		}
		n = n.copy()
		n.flags = t.newFlag()
		n.Children[key[0]] = nn
		return true, n, nil

	case nil:
		// New short node is created and track it in the tracer. The node identifier
		// passed is the path from the root node. Note the valueNode won't be tracked
		// since it's always embedded in its parent.
		t.tracer.onInsert(prefix)

		return true, &shortNode{key, value, t.newFlag()}, nil

	case hashNode:
		// We've hit a part of the trie that isn't loaded yet. Load
		// the node and insert into it. This leaves all child nodes on
		// the path to the value in the trie.
		rn, err := t.resolveAndTrack(n, prefix)
		if err != nil {
			return false, nil, err
		}
		dirty, nn, err := t.insert(rn, prefix, key, value)
		if !dirty || err != nil {
			return false, rn, err
		}
		return true, nn, nil

	default:
		panic(fmt.Sprintf("%T: invalid node: %v", n, n))
	}
}

1、key 的长度为零表示已经达到 Trie 结构的叶子节点。在 Trie 中,每个键值对数据都是存储在树的叶子节点上的,而每个叶子节点对应一个特定的键。当在 Trie 中执行插入操作时,会按照键的每个字节依次遍历树的分支,直到找到适当的叶子节点来存储数据,其中,prefix表示从根节点到当前节点的路径。

当 key 的长度为零时,表示已经没有更多的字节需要遍历,当前遍历到了 Trie 结构的底部,即到达了叶子节点。这种情况下,就可以在当前叶子节点存储或更新与该键关联的值。

当 len(key) == 0 时,表示已经到达了 Trie 的叶子节点,可以处理与叶子节点相关的逻辑,比如更新现有值节点或者创建新的值节点。这就是为什么在 insert 方法中使用 if len(key) == 0 来处理已经到达叶子节点的情况。

shortNode:

2、prefixLen(key, n.Key),计算了当前节点 n 的键 n.Key 与待插入键 key 的相同前缀的长度,用变量 matchlen 来保存。放到后面讲

3、append(prefix, key[:matchlen]...),它看起来很复杂,其实很好理解:key[:matchlen]表示key截取到前matchlen个byte,而因为是往下插入子节点,所以前缀要往前进key[:matchlen]个长度,进入到下一个键前缀。

在 append(prefix, key[:matchlen]...) 这个语句中,key[:matchlen] 表示从切片 key 中的开头取出前 matchlen 个元素,然后 ... 将这些元素展开为单独的参数。

例如,假设:

key := []byte{1, 2, 3, 4, 5}
matchlen := 3
result := append(prefix, key[:matchlen]...) 

在这种情况下,result 将包含 [prefix[0], prefix[1]……prefix[n], key[0], key[1], key[2]],即将 prefix 和 key 的前 matchlen 个元素合并在一起。

4、&shortNode{n.Key, nn, t.newFlag()}

  • n.Key:这是原始节点 n 的键部分。在当前操作中,我们已经确认了待插入的键的前缀与当前节点的键部分完全匹配,所以我们保持了这个键部分。
  • nn:这是通过递归插入操作获得的新的值子节点。这个新节点可能是原始值子节点 n.Val 更新后的节点,或者是创建的新节点。这取决于具体的插入操作。
  • t.newFlag():这是一个新的标志,用于标识新创建的节点。这个标志将用于维护 Trie 数据结构的状态。

综合起来,这行代码创建了一个新的 shortNode 节点,用于更新当前节点 n 的值子节点。新节点继承了原始节点的键部分,使用了更新后的值子节点 nn,以及一个新的标志。

5、var err error:声明一个err变量。

6、对于prefix设置为append(prefix, n.Key[:matchlen+1]...),我们做一个假设。

假设 n.Key 为 [30, 40, 50, 60, 70] 并且 matchlen 为 1 时,我们要在 Trie 中插入一个节点到位置 [30, x, y, z],其中 x, y, z 是待插入键的后续部分。我们需要构建正确的插入位置,同时保持 Trie 数据结构的正确性。

在这个示例中,假设:

  • n.Key:[30, 40, 50, 60, 70],它的键值是从它自己当前位置出发的,而不是从根节点位置出发,所以函数引用prefix的原因is to confirm the root, 保证根节点的位置是已知的
  • matchlen:1

首先,我们需要构建插入的位置。我们使用 prefix 表示从根节点到当前节点的路径。对于这个示例,假设 prefix 是 [10, 20](这是一个从根节点到当前节点的示例路径,实际上可能是不同的)。

然后,我们要在位置 [30, x, y, z] 插入新节点。为了构建这个位置,我们将以下部分合并在一起:

  • prefix:[10, 20](从根节点到当前节点的路径)
  • n.Key[:matchlen+1]:[30](当前节点的键的前 1 个元素)

通过将这两部分组合,我们得到的完整键为 [10, 20, 30],这是我们想要插入节点的位置。

接下来,我们在这个位置插入新节点,并继续维护 Trie 数据结构。这个过程会在插入操作中创建新节点并更新现有节点,以便正确地处理分支和子节点。

要搞懂其中什么位置用key[matchlen], or Key[matchlen+1],放到后面来讲

7、_, branch.Children[n.Key[matchlen]], err =t.insert(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val)

这个操作是为了让新创建的节点branch能够指向当前节点在匹配位置之后的子节点,所以用插入操作。此时因为新创建的节点子节点位置都是空的,我们需要的是在这个位置插入一个新节点,而不是在现有节点上进行更新,所以用nil作为node。所以插入位置的prefix=append(prefix, n.Key[:matchlen+1]...),就是更新根节点到要插入节点的路径,n,Key[matchlen+1:]表示待插入key的剩余部分,从匹配长度之后的位置开始;n.Val是当前节点的值子节点,这个命令是向新节点中插入匹配位置节点的子节点。

8、_, branch.Children[key[matchlen]], err = t.insert(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value)

这个操作是为了让新创建的节点branch能够指向以待插入键匹配位置之后为索引的子节点,所以它存储在Children数组里面的序号是key[matchlen],同样调用insert()函数,node=nil,prefix=append(prefix, key[:matchlen+1]...),前缀值是一样的,因为匹配的位置是一样的。后面虽然说匹配的键值不同了,但是原理是相同的,key=key[matchlen+1:],此时值节点为最开始主函数导入的节点value。

9、假设有以下条件:

  • n.Key:[30, 40, 50, 60, 70]
  • key:[30, 55, 65, 75]
  • matchlen:1(因为在第一个字节处相同)

在这种情况下,matchlen 是 1,表示 n.Key 和 key 在第一个字节处相同。那么根据这个条件,我们可以进一步解释这两个子节点的位置。

  1. branch.Children[n.Key[matchlen]]:这里的 n.Key[matchlen] 就是 40,所以这个子节点的位置是 branch.Children[40]。
  2. branch.Children[key[matchlen]]:这里的 key[matchlen] 是 55,所以这个子节点的位置是 branch.Children[55]。

基于这个示例,我们可以看到,两个子节点的位置在分支节点的子节点映射中是不同的。这是因为 matchlen 反映了当前节点键 n.Key 和待插入键 key 在某一位置处的差异,从而影响了两个子节点的位置。

10、t.tracer.onInsert(append(prefix, key[:matchlen]...))放到后面讲

hanshNode:

11、rn, err := t.resolveAndTrack(n, prefix):调用 t.resolveAndTrack 来加载当前哈希节点 n,并将其解析为实际的节点 rn。解析过程可能需要从数据库中加载节点的内容,以便进一步执行插入操作。放到后面讲

default:

12、panic(fmt.Sprintf("%T: invalid node: %v", n, n))

  • %T 是一个格式化占位符,用于输出变量的类型。
  • %v 是一个格式化占位符,用于输出变量的值。

在这个命令中,fmt.Sprintf 函数将会将节点类型 n 和节点值 n 格式化为字符串,然后作为参数传递给 panic 函数。

流程:函数为Trie类型,接收node类型参数n,前缀prefix,[]byte类型参数key,node类型参数value,value表示要插入值的节点,函数返回bool值,一个新节点node和可能出现的错误报告;

1、如果key的长度为0,表示到达了Trie结构的叶子节点,那么如果当前节点n成功转换成值节点,也即当前节点是valueNode值节点,那么赋值给v,ok=true;使用bytes.Equal()函数来比较当前节点与value值节点的值是否相等,如果不相等,则返回的bool值为true,表示需要更新,返回第二个值为value,第三个为nil表示无出错。如果不能成功转换即不是值节点,则ok为false,表示节点需要更新,函数直接返回true,value,nil。

2、如果key的长度不为0,则根据当前节点的类型进入不同的分支。

(1)若为shortNode类型, 计算待插入键key与当前节点n的键n.Key相同前缀的长度,赋值给matchlen;

如果完全相等,表示两个键在当前节点处完全匹配,此时只需要更新节点的值,保持节点不变,在这种情况下,插入操作需要在当前节点上进行更新,因为待插入的键key已经在这个节点上找到了一个完全匹配的位置。所以,我们需要操作的是当前节点往下插入,在前面shortNode-struct中,有一个成员变量n.Val,表示当前节点的子节点;append()函数将prefix往下移动key[:matchlen]个字节,即键前缀指向下一个字节;key为key[matchlen:],要插入的值节点同样为value;这四个变量为输入变量,递归调用insert()函数,函数返回赋值给dirty(bool类型),nn(node类型),err;如果dirty==false即插入操作没有改变节点或者err不为空,则函数返回false,n,err;表示未更新;如果操作成功,则函数返回三个值:true;创建的一个新的shortNode节点,new.Key仍然是原来的Key,new.Val指向新的子节点,new.flags为新创建,这个环节很像链表插入操作的指针更新;nil。

如果不完全相等,则创建一个新的分支节点branch,节点类型为fullNode,branch.flags为新创建;该分支节点包含两个子节点Children node,一个是当前节点键n.Key[matchlen]即匹配位置之后的子节点,另一个是以待插入键匹配位置之后为索引的子节点。这样,分支节点就会连接到两个不同的子节点。

如果完全不相等,matchlen=0,表示当前节点的键n.Key与待插入键key没有任何匹配,即当前节点应该成为树的新分支的根节点。则函数返回true表示需要更新,新的分支节点branch,nil;将新创建的分支节点的标识符添加到跟踪器tracer中,记录该节点的路径。

最后,若matchlen≠0,表示需要将当前节点替换为一个新的短节点,先重新思考一下链表的插入操作。所以函数返回true,新创建的短节点,其中Key=key[:matchlen]表示节点的位置,Val=branch,修改了它下一个值节点指向先前定义的branch,flags=t.newFlag();实际上最开始插入的节点只有一个value node,所以不考虑后续有链的问题。

(2)若为fullNode类型,递归调用insert()函数,其中node=n.Children[key[0]], 表示当前节点子节点中与待插入键key的第一个字节对应的子节点。prefix=append(prefix,key[0]),表示当前节点的键与待插入键的第一个字节合并成的一个新键。key=key[1:], 键值同时也要更新。值节点为value。递归返回赋值给dirty(bool), nn(node), err; 如果dirty==false或者err!=nil,即插入操作中没有发生节点插入,或者出错,函数返回false,n,err;如果插入成功,使用n=n.copy()将节点拷贝一份,确保不会影响Trie,为副本的节点创建一个新的flags,节点在Children[key[0]]位置的子节点更新为插入操作返回的新节点nn;函数返回true,n,nil;

(3)若为nil,即当前节点不存在,则创建一个新的shortNode节点作为子节点,将待插入节点的prefix追踪到tracer中,函数返回新创建的节点;

(4)若为hashNode类型,则需要先对节点进行解析,将解析完的节点完整内容赋值给rn,如果出错,则返回false,nil,err;如果成功解析,递归调用insert()函数,其中node=rn,prefix保持不变,因为本身只是先解析了一下节点再进行插入,插入的位置是不变的;同理,key,value都不变。递归返回赋值给dirty(bool), nn(插入更新后的节点), err;如果dirty==false或者err!=nil,即插入操作中没有发生节点插入,或者出错,函数返回false,rn,err;如果插入成功,则返回true, nn, nil;

(5)如果节点类型未知,则引发panic错误报告。

总体来说,这段代码实现了在Trie结构下,对节点进行插入操作。