本文详解 go 语言中递归填充嵌套结构体切片(如树形数据)的常见陷阱与正确写法,重点解决因切片重置、值拷贝及指针误用导致子节点无法回传的问题,并提供可直接运行的修复方案。
在 Go 中实现类似“部门-子部门”或“职位-子职位”的树形数据结构时,递归查询并组装层级关系是典型场景。但许多开发者(尤其从 C# 等引用语义语言转来)会遇到一个经典问题:子节点在递归函数内部成功填充,返回父级后却为空。根本原因在于 Go 的切片虽为引用类型,但其底层仍由 ptr、len、cap 三元组构成——当对结构体字段(如 u.Items)执行 u.Items = make([]Title, 0) 时,实际是覆盖了当前结构体持有的切片头,而该操作不会影响调用方持有的原始结构体副本。
原代码中存在两个关键错误:
无意义地重置切片:
u.Items = make([]Title, 0) // ❌ 错误:清空并新建切片,丢弃已有内容(即使为空)
实际上,append(nilSlice, item) 完全合法

使用值类型切片 []Title 导致深层修改失效:
当执行 u.Items = append(u.Items, *item) 时,*item 是 Title 值拷贝。若后续递归中修改 item.Items(例如追加孙子节点),这些变更不会反映到父级 u.Items[i] 中,因为 u.Items[i] 是独立副本。
推荐将嵌套字段改为 []*Title,并移除手动 make:
type Title struct {
Id string `json:"id"`
Name string `json:"name"`
Items []*Title `json:"items"` // ✅ 改为指针切片,确保深层修改可见
}对应修复后的递归方法:
func (db *DalBase) TitleChildrenRecursive(tx *gorp.Transaction, u *Title) error {
var dbChildren []entities.Title
_, err := tx.Select(&dbChildren, "SELECT * FROM title WHERE idparent = $1 ORDER BY name", u.Id)
if err != nil {
return err
}
// ✅ 移除 u.Items = make(...), 直接追加(nil 切片可安全 append)
for i := range dbChildren {
currItem := &dbChildren[i]
child := &Title{
Id: currItem.Id,
Name: currItem.Name,
}
// ✅ 递归填充子树
if err := db.TitleChildrenRecursive(tx, child); err != nil {
return err
}
u.Items = append(u.Items, child) // ✅ 追加指针,父子引用一致
}
return nil
}主调用方法同步更新(注意 items 类型需匹配):
func (db *DalBase) TitleAllChildren(tx *gorp.Transaction) ([]*Title, error) {
var dbChildren []entities.Title
_, err := tx.Select(&dbChildren, "SELECT * FROM title WHERE idparent IS NULL ORDER BY name")
if err != nil {
return nil, err
}
items := make([]*Title, 0, len(dbChildren))
for i := range dbChildren {
currItem := &dbChildren[i]
item := &Title{
Id: currItem.Id,
Name: currItem.Name,
}
if err := db.TitleChildrenRecursive(tx, item); err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}通过以上调整,递归过程中所有层级的 Items 修改都将正确回溯至根节点,最终得到完整、可序列化的树形数据。