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

Go语言中如何更简洁实现前端JSON到API结构体的归一化?

优化Go API的JSON数据归一化处理

问题背景

作为Go语言新手,当前处理前端传入API的JSON数据时,先将载荷转存为map[string]interface{},再手动做类型转换和归一化,最后转成model.Expense结构体。希望找到更简洁、更符合Go编程习惯的实现方式。

原model.Expense结构体:

type Expense struct {
    Id        primitive.ObjectID   `json:"_id,omitempty" bson:"_id,omitempty"`
    Name      string               `json:"name"`
    Frequency int                  `json:"frequency"`
    StartDate *time.Time           `json:"startDate"`
    EndDate   *time.Time           `json:"endDate,omitempty"`
    Cost      primitive.Decimal128 `json:"cost"`
    Paid      []string             `json:"paid,omitempty"`
}

原控制器核心逻辑(手动转map处理):

func InsertOneExpense(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Allow-Control-Allow-Methods", "POST")

    var expense map[string]interface{}
    json.NewDecoder(r.Body).Decode(&expense)

    var expenseName string
    if name, ok := expense["name"]; ok {
        expenseName = fmt.Sprintf("%v", name)
    } else {
        json.NewEncoder(w).Encode("missing required name")
    }

    var expenseFrequency int
    if frequency, ok := expense["frequency"]; ok {
        expenseFrequency = int(frequency.(float64))
    } else {
        expenseFrequency = 1
    }

    // 处理startDate归一化
    var expenseStartDate *time.Time
    if startDate, ok := expense["startDate"]; ok {
        startDateString := fmt.Sprintf("%v", startDate)
        startDateParsed, err := time.Parse("2006-01-02 15:04:05", startDateString)

        if err != nil {
            log.Fatal(err)
        }

        expenseStartDate = &startDateParsed
    } else {
        json.NewEncoder(w).Encode("missing required startDate")
    }

    // 处理endDate归一化
    var expenseEndDate *time.Time
    if endDate, ok := expense["endDate"]; ok {
        endDateString := fmt.Sprintf("%v", endDate)
        endDateParsed, err := time.Parse("2006-01-02 15:04:05", endDateString)

        if err != nil {
            log.Fatal(err)
        }

        expenseEndDate = &endDateParsed
    } else {
        expenseEndDate = nil
    }

    // 处理cost归一化
    var expenseCost primitive.Decimal128
    if cost, ok := expense["cost"]; ok {
        costString := fmt.Sprintf("%v", cost)
        costPrimitive, err := primitive.ParseDecimal128(costString)

        if err != nil {
            log.Fatal(err)
        }

        expenseCost = costPrimitive
    } else {
        json.NewEncoder(w).Encode("missing required cost")
        return
    }

    normalizedExpense := model.Expense{
        Name:      expenseName,
        Frequency: expenseFrequency,
        StartDate: expenseStartDate,
        EndDate:   expenseEndDate,
        Cost:      expenseCost,
    }

    // 对结构体变量执行后续操作...
}

优化方案

1. 直接解码到结构体,自定义特殊字段解析

无需转存为map,直接将JSON解码到Expense结构体,通过实现json.Unmarshaler接口处理特殊类型(时间、Decimal128)的自动转换,避免手动类型断言。

修改后的Expense结构体及自定义解析方法:

package model

import (
    "encoding/json"
    "fmt"
    "time"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

type Expense struct {
    Id        primitive.ObjectID   `json:"_id,omitempty" bson:"_id,omitempty"`
    Name      string               `json:"name"`
    Frequency int                  `json:"frequency"`
    StartDate *time.Time           `json:"startDate"`
    EndDate   *time.Time           `json:"endDate,omitempty"`
    Cost      primitive.Decimal128 `json:"cost"`
    Paid      []string             `json:"paid,omitempty"`
}

// 自定义StartDate的JSON解析,兼容前端传入的字符串格式
func (t *time.Time) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    parsedTime, err := time.Parse("2006-01-02 15:04:05", s)
    if err != nil {
        return err
    }
    *t = parsedTime
    return nil
}

// 自定义Cost的JSON解析,将数字/字符串转为Decimal128
func (d *primitive.Decimal128) UnmarshalJSON(data []byte) error {
    var s interface{}
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    var str string
    switch v := s.(type) {
    case string:
        str = v
    case float64:
        str = fmt.Sprintf("%f", v)
    default:
        return fmt.Errorf("invalid cost type")
    }
    dec, err := primitive.ParseDecimal128(str)
    if err != nil {
        return err
    }
    *d = dec
    return nil
}

2. 简化必填字段验证与默认值处理

解码后统一做必填字段校验,默认值直接在结构体初始化时设置,无需逐个字段判断。

3. 错误处理优化

替换log.Fatal为HTTP错误响应,避免进程意外退出,同时统一返回JSON格式错误信息;修正原代码中CORS头的拼写错误(Allow-Control-Allow-Methods → Access-Control-Allow-Methods)。


优化后的控制器代码

func InsertOneExpense(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Access-Control-Allow-Methods", "POST")

    var expense model.Expense
    // 设置Frequency默认值
    expense.Frequency = 1

    // 直接解码到结构体
    if err := json.NewDecoder(r.Body).Decode(&expense); err != nil {
        http.Error(w, jsonError("invalid request body: "+err.Error()), http.StatusBadRequest)
        return
    }

    // 必填字段校验
    if expense.Name == "" {
        http.Error(w, jsonError("missing required field: name"), http.StatusBadRequest)
        return
    }
    if expense.StartDate == nil {
        http.Error(w, jsonError("missing required field: startDate"), http.StatusBadRequest)
        return
    }
    // 检查Cost是否为零值(primitive.Decimal128零值表示未设置)
    if expense.Cost.IsZero() {
        http.Error(w, jsonError("missing required field: cost"), http.StatusBadRequest)
        return
    }

    // 后续操作(如存入数据库)...
    // example: result, err := collection.InsertOne(context.TODO(), expense)

    json.NewEncoder(w).Encode(map[string]interface{}{
        "status": "success",
        "data":   expense,
    })
}

// 辅助函数:返回JSON格式错误信息
func jsonError(msg string) string {
    b, _ := json.Marshal(map[string]string{"error": msg})
    return string(b)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 10:49:55