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

Go+Fiber+MongoDB中如何实现动态结构体及类似JS展开操作?

解决方案

针对你遇到的两个问题,直接用Go的map[string]interface{}就能搞定,不需要动态结构体,具体实现如下:

1. 用Map模拟动态字段结构

Go本身不支持动态添加字段的结构体,但map[string]interface{}可以完美替代——它既能容纳固定字段,也能灵活添加Variant里的动态键值对。

2. 展开Variants到根层级(类似JS展开运算符)

核心思路是:先把Product的固定字段转成基础Map,再遍历每个Variant,将Variant的键值对合并到基础Map中,每个合并后的Map就是CSV的一行数据。

完整代码示例

import (
    "encoding/csv"
    "fmt"
    "io"
    "sort"
    "strings"
)

type Product struct {
    Name        string              `json:"name"`
    Code        string              `json:"code"`
    Description string              `json:"description"`
    Brand       string              `json:"brand"`
    Variants    []map[string]string `json:"variants"`
    Categories  []int               `json:"categories"`
}

// 将Product固定字段转为基础Map
func productToBaseMap(p Product) map[string]interface{} {
    // 把Categories数组转成逗号分隔的字符串,适配CSV格式
    categoriesStr := strings.Join(strings.Fields(fmt.Sprint(p.Categories)), ",")
    return map[string]interface{}{
        "Name":        p.Name,
        "Code":        p.Code,
        "Description": p.Description,
        "Brand":       p.Brand,
        "Categories":  categoriesStr,
    }
}

// 展开所有Variants,生成每行的完整数据Map
func expandProductVariants(p Product) []map[string]interface{} {
    baseMap := productToBaseMap(p)
    var expandedRows []map[string]interface{}

    for _, variant := range p.Variants {
        // 复制基础Map,避免修改原数据
        row := make(map[string]interface{})
        for k, v := range baseMap {
            row[k] = v
        }
        // 合并Variant的动态字段到根层级(等效JS的...variants)
        for k, v := range variant {
            row[k] = v
        }
        expandedRows = append(expandedRows, row)
    }

    return expandedRows
}

// 收集所有CSV表头(固定字段+所有动态字段)
func collectAllHeaders(expandedRows []map[string]interface{}) []string {
    headerSet := make(map[string]bool)
    for _, row := range expandedRows {
        for k := range row {
            headerSet[k] = true
        }
    }

    // 转成有序切片,保证表头顺序一致
    headers := make([]string, 0, len(headerSet))
    for k := range headerSet {
        headers = append(headers, k)
    }
    sort.Strings(headers)
    return headers
}

// 将展开后的数据写入CSV
func WriteProductCSV(w io.Writer, products []Product) error {
    var allExpandedRows []map[string]interface{}
    for _, p := range products {
        rows := expandProductVariants(p)
        allExpandedRows = append(allExpandedRows, rows...)
    }

    if len(allExpandedRows) == 0 {
        return nil
    }

    headers := collectAllHeaders(allExpandedRows)
    writer := csv.NewWriter(w)
    defer writer.Flush()

    // 写入表头
    if err := writer.Write(headers); err != nil {
        return fmt.Errorf("write headers failed: %w", err)
    }

    // 写入每行数据
    for _, row := range allExpandedRows {
        record := make([]string, len(headers))
        for i, header := range headers {
            val, exists := row[header]
            if exists {
                record[i] = fmt.Sprint(val)
            } else {
                record[i] = "" // 无对应值则填空字符串
            }
        }
        if err := writer.Write(record); err != nil {
            return fmt.Errorf("write record failed: %w", err)
        }
    }

    return nil
}

使用示例

// 模拟测试数据
products := []Product{
    {
        Name:        "Rock",
        Code:        "RRR",
        Description: "This is rock",
        Brand:       "R",
        Categories:  []int{1, 2},
        Variants: []map[string]string{
            {"Size": "M", "Color": "Red"},
            {"Size": "L", "Color": "Blue"},
        },
    },
    {
        Name:        "Rambo",
        Code:        "RAM",
        Description: "This is Rambo",
        Brand:       "RA",
        Categories:  []int{3},
        Variants: []map[string]string{
            {"Size": "S", "Color": "Black"},
            {"Size": "XL", "Color": "White"},
        },
    },
}

// 写入到文件(也可以直接响应到Fiber的ctx.Response())
file, err := os.Create("products.csv")
if err != nil {
    panic(err)
}
defer file.Close()

if err := WriteProductCSV(file, products); err != nil {
    panic(err)
}

关键说明

  • 复制基础Map时用了浅拷贝,因为Product的固定字段都是基本类型/字符串,不会有引用类型的副作用;如果后续有复杂嵌套字段,需要改用深拷贝。
  • Categories数组转成逗号分隔字符串是为了适配CSV的单字段格式,你可以根据需求调整这个逻辑。
  • 收集表头时做了排序,确保每次生成的CSV表头顺序一致,避免混乱。

内容的提问来源于stack exchange,提问作者Saravanan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 03:25:30