You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 18:35:28