Tree
tree 包提供泛型树构建器,将扁平切片转换为层级树结构。
快速开始
import "github.com/coldsmirk/vef-framework-go/tree"
type Department struct {
ID string `json:"id"`
ParentID *string `json:"parentId"`
Name string `json:"name"`
Children []Department `json:"children"`
}
// 定义适配器
adapter := tree.Adapter[Department]{
GetID: func(d Department) string { return d.ID },
GetParentID: func(d Department) *string { return d.ParentID },
GetChildren: func(d Department) []Department { return d.Children },
SetChildren: func(d *Department, children []Department) { d.Children = children },
}
// 从扁平切片构建树
roots := tree.Build(flatDepartments, adapter)
适配器
Adapter[T] 结构体定义了树构建器如何访问节点属性:
type Adapter[T any] struct {
GetID func(T) string // 提取节点 ID
GetParentID func(T) *string // 提取父级 ID(nil = 根节点)
GetChildren func(T) []T // 获取子节点切片
SetChildren func(*T, []T) // 设置子节点切片
}
公开 surface:
| API | Signature |
|---|---|
Adapter[T] | type Adapter[T any] struct |
Adapter.GetID | func(T) string |
Adapter.GetParentID | func(T) *string |
Adapter.GetChildren | func(T) []T |
Adapter.SetChildren | func(*T, []T) |
Build | tree.Build[T any](nodes []T, adapter tree.Adapter[T]) []T |
FindNode | tree.FindNode[T any](roots []T, targetID string, adapter tree.Adapter[T]) (T, bool) |
FindNodePath | tree.FindNodePath[T any](roots []T, targetID string, adapter tree.Adapter[T]) ([]T, bool) |
Build 契约
Build 将扁平切片转换成嵌套根节点列表。
关键规则:
Build(nil, adapter)和Build([]T{}, adapter)返回非 nil 空切片 ([]T{})GetID的值按原始 string key 使用;特殊字符和 Unicode 不会被 normalize 或 escapeGetID应返回唯一且非空的 ID;空 ID 节点不会进入 parent lookup 的索引, 其自身 children 也不会被填充- 空 ID 节点仍可能因为自身 parent 关系出现在返回的 roots 中,或出现在某个 parent 的 children 中
GetParentID(node) == nil时,该节点是 root- 非 nil parent ID 如果不存在于已索引 node map,也会让该节点成为 root
- 如果一组节点形成 parent chain 永远到不了 root 的闭环,它们不会出现在返回的 roots 中
Build在设置 children 时使用 visited tracking,避免 cyclic parent data 无限递归Build会对输入 slice 的元素调用SetChildren,然后返回 root 元素的值拷贝; 调用方应把输入 slice 元素视为可被修改Build不会调用GetChildren,所以只用于构建树的 wrapper 可以不提供它- adapter callback 缺失时,代码执行到对应 callback 会自然 panic
查找节点
FindNode
在树中按 ID 搜索节点:
node, found := tree.FindNode(roots, "dept-123", adapter)
if found {
fmt.Println(node.Name)
}
契约:
- 空
targetID返回T的零值和false - 目标不存在时也返回
T的零值和false - 遍历是 depth-first,并沿着
GetChildren返回的 slice 继续 - duplicate IDs 不会被去重;返回第一个遍历命中的节点
FindNode不会围绕GetChildren额外加 cycle protection,因此应传入无环树
FindNodePath
获取从根节点到目标节点的完整路径:
path, found := tree.FindNodePath(roots, "dept-456", adapter)
if found {
for _, node := range path {
fmt.Println(node.Name) // 输出:根节点 → 父节点 → dept-456
}
}
契约:
- 空
targetID、目标不存在或空树都会返回nil, false - 命中目标时返回完整 root-to-node path 和
true - 遍历是 depth-first,并沿着
GetChildren返回的 slice 继续 FindNodePath不会围绕GetChildren额外加 cycle protection,因此应传入无环树
框架集成
tree 包被 CRUD 的 FindTree 构建器使用。NewFindTree[T, S] 需要一个 func([]T) []T 形状的 builder,所以你写一个薄包装,把模型的 adapter 闭进去:
func buildDepartmentTree(flat []Department) []Department {
adapter := tree.Adapter[Department]{
GetID: func(d Department) string { return d.ID },
GetParentID: func(d Department) *string { return d.ParentID },
SetChildren: func(d *Department, children []Department) { d.Children = children },
// GetChildren 只在调用 tree.FindNode / tree.FindNodePath 时使用;
// tree.Build 本身不会调它。
GetChildren: func(d Department) []Department { return d.Children },
}
return tree.Build(flat, adapter)
}
// 然后把这个 wrapper 传给 CRUD builder。
crud.NewFindTree[Department, DepartmentSearch](buildDepartmentTree)
tree.Build的签名是Build[T any](nodes []T, adapter Adapter[T]) []T,多了adapter参数,不能直接传给NewFindTree,需要靠上面的 wrapper 桥接。 当GetID返回""时,该节点会被跳过索引;它的 children 也不会被填充。