最稳妥方案是前序遍历+缩进文本格式。每行一个节点,缩进空格数=层级×2,字段用\t分隔,天然支持grep、awk、excel等工具,追加节点只需写一行,结构肉眼可读,避免json嵌套不一致、解析脆弱及全量重写问题。

直接用递归遍历 + 换行分隔写入最稳妥,别碰 JSON 数组嵌套或自定义二进制格式——文件是给人和程序共读的,可读性、追加能力和调试成本比“看起来紧凑”重要得多。
为什么不用 json.Marshal([]interface{}) 写树形结构到文件
Go 的 json.Marshal 对嵌套 slice 或含 nil 子节点的树会输出不一致结构(比如某层是 []interface{},下一层突然变成 map[string]interface{}),导致后续用 jq 或 Python 脚本解析时字段位置不可靠;更关键的是,JSON 本身不带层级标记,你无法从单行内容判断它是根节点、二级子节点还是叶子节点。
- 写入后无法按层级 grep:比如想提取所有 level=2 的节点,JSON 行里没有显式 level 字段
- 追加新节点困难:得先读全文件、解码、插入、再重写整个文件,O(n) 开销
- 出错难定位:某一行 JSON 格式错,整个
json.Unmarshal失败,连出错行号都得靠第三方库猜
用前序遍历 + tab 缩进写入文本文件
这是最轻量、最易 debug 的方案。每个节点一行,缩进空格数 = 层级 × 2,字段用制表符 \t 分隔,兼容 Excel / awk / cat / head 等所有 Unix 工具。
- 层级信息内建在缩进中,
awk -F'\t' '{print NF-1}' file.txt就能算出每行 level - 新增节点只需追加一行,无需重载全量数据
- 用
vim或 VS Code 打开即见树形缩进,肉眼可验证结构正确性
示例代码:
func flattenTreeToFile(root *Node, f *os.File) error {
var walk func(*Node, int)
walk = func(n *Node, level int) {
indent := strings.Repeat(" ", level)
line := fmt.Sprintf("%s%s\t%s\t%d\n", indent, n.ID, n.Name, n.Level)
_, err := f.WriteString(line)
if err != nil {
return
}
for _, child := range n.Children {
walk(child, level+1)
}
}
walk(root, 0)
return nil
}
如果必须用结构化格式:选 CSV 而非 JSON
CSV 显式声明字段,且天然支持单行追加。只要约定好字段顺序(如 id,parent_id,name,level,full_path),就能用 csv.Reader 流式读、csv.Writer 追加写,内存占用恒定 O(1)。
-
full_path字段存类似/root/go/stdlib的路径字符串,方便后续用strings.HasPrefix做前缀过滤 - 避免用逗号做分隔符——名称里可能含逗号;改用
\t或|,并在写入前用strings.ReplaceAll(name, "\t", " ")清洗 - 首行写 header(如
id\tparent_id\tname\tlevel\tfull_path),让head -n1 file.csv一眼看懂字段语义
容易被忽略的边界点:空 children 和循环引用
真实树结构常有 Children == nil 或 Children = []*Node{} 两种空状态,遍历时若只判 n.Children == nil 会漏掉后者;更危险的是,某些配置树存在人为错误的父子闭环(比如 A → B → C → A),递归遍历会栈溢出。
- 统一用
len(n.Children) == 0判空,兼容两种情况 - 加深度限制:递归函数传入
maxDepth int,每次调用if level > maxDepth { log.Warn("tree too deep at node ", n.ID); return } - 生产环境建议加指针地址缓存:
seen map[uintptr]bool,每次进入节点前检查uintptr(unsafe.Pointer(n))是否已存在
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











