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

