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
相关产品推荐
相关产品推荐

