Go语言如何递归迭代带ParentId的子菜单生成嵌套JSON?
解决Go语言中多层嵌套菜单的迭代展示问题
我看了你的代码和返回结果,核心问题出在递归修改子菜单时操作的是值拷贝而非原数组元素,另外当前逐次查询数据库的方式也不够高效。下面给你两种解决方案:
方案一:修复现有递归逻辑,正确修改原菜单元素
你的iterateChild函数里,遍历resChild时用了for _, sv := range resChild,这里的sv是每个子菜单的值拷贝,修改&sv的Children属性并不会影响原resChild数组里的元素,所以二级及更深的子菜单无法被赋值到最终响应中。
修改后的代码如下:
func iterateChild(ctx context.Context, menuCollection *mongo.Collection, obj *model.ResponseMenu) { // 查询当前菜单的直接子菜单 var resSc []model.ResponseMenu fc := bson.M{"parent_id": obj.ID.Hex()} cursor, err := menuCollection.Find(ctx, fc) if err != nil { fmt.Printf("查询子菜单失败: %v\n", err) return } defer cursor.Close(ctx) // 及时关闭cursor,避免资源泄漏 if err = cursor.All(ctx, &resSc); err != nil { fmt.Printf("解析子菜单失败: %v\n", err) return } // 直接将子菜单赋值给当前菜单的Children obj.Children = resSc // 递归处理每个子菜单的子菜单(用索引遍历操作原数组元素) for i := range obj.Children { iterateChild(ctx, menuCollection, &obj.Children[i]) } } func GetListMenu(c *gin.Context) { var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) defer cancel() // 统一管理上下文销毁,避免遗漏 var response []model.ResponseMenu q := c.Query("q") perPage, err := strconv.Atoi(c.Query("per_page")) if err != nil || perPage == 0 { perPage = 10 } page, _ := strconv.Atoi(c.Query("page")) if page > 0 { page -= 1 } filter := bson.M{ "title": primitive.Regex{ Pattern: q, Options: "i", }, "is_parent": true, "parent_id": "", } options := options.Find() options.SetSkip(int64(page * perPage)) options.SetLimit(int64(perPage)) options.SetSort(bson.D{{"created_at", -1}}) cursor, err := menuCollection.Find(ctx, filter, options) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "status": false, "message": http.StatusInternalServerError, "error": err.Error(), }) return } defer cursor.Close(ctx) if err = cursor.All(ctx, &response); err != nil { log.Fatal(err) c.JSON(http.StatusInternalServerError, gin.H{ "status": false, "message": err.Error(), }) return } // 遍历父菜单,递归填充子菜单 for i := range response { iterateChild(ctx, menuCollection, &response[i]) } // 返回结果 if len(response) > 0 { c.JSON(http.StatusOK, gin.H{ "status": true, "message": "Success retrieve data", "data": response, }) } else { c.JSON(http.StatusOK, gin.H{ "status": false, "message": "Data not found", "data": response, }) } }
修改要点:
- 将
ctx和menuCollection作为参数传入递归函数,避免重复创建上下文,减少资源泄漏风险。 - 用
for i := range obj.Children遍历,直接操作原数组的元素指针,确保子菜单的修改能同步到最终响应中。 - 统一管理cursor和上下文的销毁逻辑,避免资源泄漏。
方案二:一次性查询所有菜单,内存中构建树形结构(更高效)
如果菜单数量较多,逐次递归查询数据库会产生大量请求,性能较差。推荐一次性查询所有符合条件的菜单,再在内存中构建树形结构:
func GetListMenu(c *gin.Context) { var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second) defer cancel() q := c.Query("q") perPage, err := strconv.Atoi(c.Query("per_page")) if err != nil || perPage == 0 { perPage = 10 } page, _ := strconv.Atoi(c.Query("page")) if page > 0 { page -= 1 } // 1. 查询分页后的父菜单 parentFilter := bson.M{ "title": primitive.Regex{ Pattern: q, Options: "i", }, "is_parent": true, "parent_id": "", } parentOptions := options.Find() parentOptions.SetSkip(int64(page * perPage)) parentOptions.SetLimit(int64(perPage)) parentOptions.SetSort(bson.D{{"created_at", -1}}) parentCursor, err := menuCollection.Find(ctx, parentFilter, parentOptions) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "status": false, "message": http.StatusInternalServerError, "error": err.Error(), }) return } defer parentCursor.Close(ctx) var parentMenus []model.ResponseMenu if err = parentCursor.All(ctx, &parentMenus); err != nil { log.Fatal(err) c.JSON(http.StatusInternalServerError, gin.H{ "status": false, "message": err.Error(), }) return } if len(parentMenus) == 0 { c.JSON(http.StatusOK, gin.H{ "status": false, "message": "Data not found", "data": parentMenus, }) return } // 2. 获取所有父菜单ID,查询它们的所有子孙菜单 var parentIDHexes []string var parentIDs []primitive.ObjectID for _, pm := range parentMenus { parentIDs = append(parentIDs, pm.ID) parentIDHexes = append(parentIDHexes, pm.ID.Hex()) } childFilter := bson.M{ "$or": []bson.M{ {"parent_id": bson.M{"$in": parentIDs}}, {"parent_id": bson.M{"$in": parentIDHexes}}, }, } childCursor, err := menuCollection.Find(ctx, childFilter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "status": false, "message": http.StatusInternalServerError, "error": err.Error(), }) return } defer childCursor.Close(ctx) var allChildMenus []model.ResponseMenu if err = childCursor.All(ctx, &allChildMenus); err != nil { log.Fatal(err) c.JSON(http.StatusInternalServerError, gin.H{ "status": false, "message": err.Error(), }) return } // 3. 构建菜单ID到菜单指针的映射,快速查找父菜单 menuMap := make(map[string]*model.ResponseMenu) for i := range parentMenus { menuMap[parentMenus[i].ID.Hex()] = &parentMenus[i] } for i := range allChildMenus { menuMap[allChildMenus[i].ID.Hex()] = &allChildMenus[i] } // 4. 把所有子菜单挂载到对应的父菜单下 for _, child := range allChildMenus { if parent, ok := menuMap[child.ParentId]; ok { parent.Children = append(parent.Children, *menuMap[child.ID.Hex()]) } } // 5. 递归构建多层嵌套结构 var buildTree func(*model.ResponseMenu) buildTree = func(menu *model.ResponseMenu) { for i := range menu.Children { buildTree(&menu.Children[i]) } } for _, pm := range parentMenus { buildTree(&pm) } // 返回结果 c.JSON(http.StatusOK, gin.H{ "status": true, "message": "Success retrieve data", "data": parentMenus, }) }
方案优势:
- 仅需2次数据库查询,大幅降低数据库请求次数,性能更优。
- 内存中构建树形结构,逻辑清晰,便于后续维护扩展。
验证效果
修改后,你的Settings菜单会正确显示它的子菜单Origo,返回的JSON中children属性将包含完整的多层嵌套结构。
内容的提问来源于stack exchange,提问作者silvia zulinka
相关产品推荐
相关产品推荐

