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

Golang Gin/Gorm容器化应用产品名称偶现空值问题排查

问题:Docker环境下Gin/Gorm应用偶现外部API获取产品名称为null

基于Golang Gin/Gorm框架开发的应用,根据数据库存储的产品ID从外部API获取产品名称时,部分产品名称偶现null值。本地运行完全正常,但Docker容器化的生产环境中出现该异常。

应用结构概述

  • Gin/Gorm框架:使用Gin作为HTTP路由框架,Gorm作为ORM进行数据库操作
  • 供应管理模块:管理与数据库中产品关联的供应信息
  • 外部API集成:通过HTTP客户端从外部API端点获取产品名称

核心转换逻辑代码

// Convert supplies to supplyResponses
supplyResponses := make([]models.SupplyResponse, len(supplies))
apiURLs := make(map[string]string)
apiURLs["FLOWER"] = os.Getenv("FLOWER_API") + "flower_type/?id="
apiURLs["SWEET"] = os.Getenv("SWEET_API") + "sweet_type/?id="
apiURLs["CONSUMABLE"] = os.Getenv("CONSUMABLE_API") + "consumable_type/?id="

for _, supply := range supplies {
    switch supply.ProductType.Name {
    case "FLOWER":
        apiURLs["FLOWER"] += supply.ProductId.String() + "&id="
    case "SWEET":
        apiURLs["SWEET"] += supply.ProductId.String() + "&id="
    case "CONSUMABLE":
        apiURLs["CONSUMABLE"] += supply.ProductId.String() + "&id="
    default:
        context.IndentedJSON(http.StatusInternalServerError, gin.H{"error": "Product type cant be found"})
        return
    }
}

for _, url := range apiURLs {
    index := strings.Index(url, "?id=")
    if index != -1 {
        if len(url) > index+len("?id=") {
            lastIndex := strings.LastIndex(url, "&id=")
            if lastIndex != -1 {
                url = url[:lastIndex]
            }
            names, err := services.GetProductNames(context, url)
            if err != nil {
                fmt.Println(err.Error())
            }

            // Loop over each supply and find its corresponding name
            for i, supply := range supplies {
                supplyResponses[i] = supply.ToSupplyResponse()
                for _, name := range names.Data {
                    if supply.ProductId == name.Id {
                        switch lang {
                        case "uz":
                            supplyResponses[i].Name = name.Name.Uz
                        case "ru":
                            supplyResponses[i].Name = name.Name.Ru
                        case "en":
                            supplyResponses[i].Name = name.Name.En
                        default:
                            responseNames := models.ProductName{
                                Uz: name.Name.Uz,
                                Ru: name.Name.Ru,
                                En: name.Name.En,
                            }
                            supplyResponses[i].Name = &responseNames
                        }
                        break
                    }
                }
            }
        }
    }
}

模型定义

package models

type Supply struct {
    ID            uuid.UUID      `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
    StockID       uuid.UUID      `gorm:"column:stock_id;type:uuid;"`
    SupplyDate    time.Time      `gorm:"column:supply_date;type:timestamp"`
    Quantity      float64        `gorm:"column:quantity;type:double precision"`
    UnitCost      float64        `gorm:"column:unit_cost;type:numeric(10,2)"`
    ProductTypeId uint           `gorm:"column:product_type_id;type:uint;"`
    ProductId     uuid.UUID      `gorm:"column:product_id;type:uuid"`
    Description   string         `gorm:"column:description;type:text"`
    CreatedAt     time.Time      `gorm:"column:created_at"`
    UpdatedAt     time.Time      `gorm:"column:updated_at"`
    DeletedAt     gorm.DeletedAt `gorm:"column:deleted_at"`

    // StockID column relation with Stock model
    Stock       Stock       `gorm:"foreignKey:StockID" json:"stock"`
    ProductType ProductType `gorm:"foreignKey:ProductTypeId" json:"product_type"`
}

// BeforeCreate hook to generate UUID for ID field and set time formats
func (supply *Supply) BeforeCreate(tx *gorm.DB) (err error) {
    supply.ID = uuid.New()
    return nil
}

type SupplyResponse struct {
    ID          uuid.UUID           `json:"id"`
    StockID     uuid.UUID           `json:"stockId"`
    SupplyDate  string              `json:"supplyDate"`
    Quantity    float64             `json:"quantity"`
    UnitCost    float64             `json:"unitCost"`
    ProductType ProductTypeResponse `json:"productType"`
    ProductId   uuid.UUID           `json:"productId"`
    Description string              `json:"description"`
    CreatedAt   string              `json:"createdAt"`
    UpdatedAt   string              `json:"updatedAt"`
    DeletedAt   string              `json:"deletedAt"`
    Stock       StockResponse       `json:"stock"`
    Name        interface{}         `json:"name"`
}

func FormatStock(stock Stock) StockResponse {
    deletedAt := ""
    if stock.DeletedAt.Valid {
        deletedAt = stock.DeletedAt.Time.Format("2006-01-02T15:04:05")
    }

    return StockResponse{
        ID:         stock.ID,
        MerchantID: stock.MerchantID,
        BranchID:   stock.BranchID,
        CreatedAt:  stock.CreatedAt.Format("2006-01-02T15:04:05"),
        UpdatedAt:  stock.UpdatedAt.Format("2006-01-02T15:04:05"),
        DeletedAt:  deletedAt,
    }
}

func (supply *Supply) ToSupplyResponse() SupplyResponse {
    deletedAt := ""
    if supply.DeletedAt.Valid {
        deletedAt = supply.DeletedAt.Time.Format("2006-01-02T15:04:05")
    }

    // Retrieve the ProductType struct from the database
    var productType ProductType
    if err := initializers.DB.First(&productType, supply.ProductTypeId).Error; err != nil {
        // Handle error if the ProductType is not found
        return SupplyResponse{}
    }

    return SupplyResponse{
        ID:          supply.ID,
        StockID:     supply.StockID,
        SupplyDate:  supply.SupplyDate.Format("2006-01-02T15:04:05"),
        Quantity:    supply.Quantity,
        UnitCost:    supply.UnitCost,
        ProductType: productType.ToProductTypeResponse(),
        ProductId:   supply.ProductId,
        Description: supply.Description,
        CreatedAt:   supply.CreatedAt.Format("2006-01-02T15:04:05"),
        UpdatedAt:   supply.UpdatedAt.Format("2006-01-02T15:04:05"),
        DeletedAt:   deletedAt,
        Stock:       FormatStock(supply.Stock),
    }
}

type IdentifySupplyResponse struct {
    QuantityTaken float64        `json:"quantityTaken"`
    Supply        SupplyResponse `json:"supply"`
}

type ProductNames struct {
    Data []struct {
        Id   uuid.UUID `json:"id"`
        Name struct {
            Uz string `json:"uz"`
            Ru string `json:"ru"`
            En string `json:"en"`
        } `json:"name"`
    } `json:"data"`
}

type ProductName struct {
    Uz string `json:"uz"`
    Ru string `json:"ru"`
    En string `json:"en"`
}

GetProductNames函数实现

func GetProductNames(context *gin.Context, url string) (models.ProductNames, error) {
    _, _, err := middlewares.CheckPermissions(context)
    if err != nil {
        context.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
        return models.ProductNames{}, err
    }

    authToken := context.GetHeader("Authorization")

    request, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return models.ProductNames{}, err
    }

    fmt.Println(url)

    request.Header.Set("Authorization", authToken)

    response, err := http.DefaultClient.Do(request)
    if err != nil {
        return models.ProductNames{}, err
    }
    defer response.Body.Close()

    if response.StatusCode != http.StatusOK {
        return models.ProductNames{}, fmt.Errorf("unexpected status code: %d", response.StatusCode)
    }

    var responseData models.ProductNames

    err = json.NewDecoder(response.Body).Decode(&responseData)
    if err != nil {
        return models.ProductNames{}, fmt.Errorf("error decoding data: %v", err.Error())
    }

    fmt.Println(responseData)

    return responseData, nil
}

问题排查与解决方案

1. 环境变量一致性检查

本地与Docker环境的API地址变量(如FLOWER_API)可能存在差异,导致请求地址无效:

  • 验证Docker容器内环境变量配置,确保API地址完整且可访问;
  • 在容器启动脚本中添加环境变量打印日志,确认配置正确性。

2. UUID格式统一

部分外部API严格区分UUID大小写,而本地与Docker环境的UUID字符串输出可能存在差异:

  • 将supply.ProductId.String()统一转为小写/大写后再拼接URL;
  • 对比本地与Docker环境中UUID的输出格式,确保一致。

3. 修复循环覆盖问题

当前代码每次遍历API时会重新生成supplyResponses,覆盖已设置的Name字段:

  • 先批量初始化所有supplyResponses,再根据API返回结果填充Name:
    // 先初始化所有响应结构
    for i, supply := range supplies {
        supplyResponses[i] = supply.ToSupplyResponse()
    }
    
    // 再遍历API获取名称并填充
    for _, url := range apiURLs {
        // ... URL处理逻辑
        names, err := services.GetProductNames(context, url)
        if err != nil {
            // 记录详细错误日志
            continue
        }
        // 匹配产品ID并设置名称
        for i := range supplyResponses {
            for _, name := range names.Data {
                if supplyResponses[i].ProductId == name.Id {
                    // ... 语言分支设置Name逻辑
                    break
                }
            }
        }
    }
    

4. 优化HTTP客户端

全局共享http.DefaultClient在高并发场景下可能引发竞争,同时缺少超时配置:

  • 为每个请求创建独立客户端或配置带超时的连接池:
    client := &http.Client{
        Timeout: 10 * time.Second,
        Transport: &http.Transport{
            MaxIdleConns:        10,
            IdleConnTimeout:     30 * time.Second,
            DisableCompression:  true,
        },
    }
    response, err := client.Do(request)
    

5. 完善错误处理

当前仅打印API错误,未中断或降级处理,导致无效数据继续流转:

  • 当GetProductNames返回错误时,记录请求URL、错误详情,并考虑是否终止请求或返回默认值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 22:39:50