写golang restful接口时遇到的一个坑

本文讲述了在编写Golang RESTful接口时遇到的一个关于切片的问题。在处理包含切片的结构体时,由于切片的引用特性,导致数据意外覆盖。通过调试和分析,找到了问题的原因及两种解决方案:一是将切片操作放入循环内部,二是将切片字段定义为空接口。此经验教训提醒开发者在使用Golang切片时需谨慎。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

话不多说,先上代码

type detail struct {
    High float64 `json:"high"` 
    Low float64 `json:"low"`
    Average float64 `json:"average"`
}
type Spot struct {
    UpdateDate string `json:"update_date"`
    Detail []detail `json:"detail"`
}
    .
    .//此处省略部分无关代码
    .
    key := constant.REDIS_INFIX_PREMIUM + productId  //此处是去redis读数据
    d, err := c.ZRevRangeByScore(key, maxScores, minScores, page, num)//取出的[][]byte放入d中
    if err != nil {
     logger.Warnning(err)
     return nil, err
    }

    var (
     prices []Spot
     p      Spot
    )

    for _, v := range d {        //遍历[][]byte
     err = json.Unmarshal(v, &p) //反序列化赋给p
     if err != nil {
        logger.Warnning(err)
        return nil, err
     }
     prices = append(prices, p) //每个价格点都加入到prices切片中,然后返回
    }

    return prices, nil


       乍一看这段代码没什么问题,但是实际上调试的时候,发现prices切片中的每个元素,也就是每个Spot对象的Detail字段都是一模一样的(UpdateDate字段没问题),一开始怀疑是存redis时出了问题,后来发现redis里的内容是对的。

让我来加入一些调试代码,把地址什么的都打印出来:

for _, v := range d {        //遍历[][]byte
     err = json.Unmarshal(v, &p) //反序列化赋给p
     fmt.Printf("p的Detail的Data的地址: %p \n",p.Sli)//每次循环打印p的Detail的Data的地址
     if err != nil {
        logger.Warnning(err)
        return nil, err
     }
     prices = append(prices, p) //每个价格点都加入到prices切片中,然后返回
    }
    fmt.Println(prices) //打印每个p的内容
    .
    .
    .
    打印结果:
    p的Detail的Data的地址: 0xc0420420c0 
    p的Detail的Data的地址: 0xc0420420c0  //指向的地址未发生改变
    [{2017-11-08 [{250 750 500} {250 750 500}]} {2017-11-09 [{250 750 500} {250 750 500}]}]  //日期后面就是Detail字段,发现都变得一模一样



       后来排查了很久,其实是因为p内部包含了一个切片,也就是Detail,然后这个切片会指向一个地址,真正的Data是存在这个地址中的,如果切片容量够的情况下,是不会改变指向的地址的。这就导致了,最后一个Spot对象的Detail切片的Data覆盖了之前append进去的所有Spot对象的Detail的Data,因为它们指向的都是一个地址,要改变当然一起改变了。看上面的地址。

       其实修改起来很简单,只要把var p Spot加到循环中,而不是放在循环外面就可以了。道理很简单,每次新建变量的时候,Detail会指向一个新的地址。

for _, v := range d {        
     var p Spot  //变量申明放入循环内
     err = json.Unmarshal(v, &p) 
     fmt.Printf("p的Detail的Data的地址: %p \n",p.Sli)//每次循环打印p的Detail的Data的地址
     if err != nil {
        logger.Warnning(err)
        return nil, err
     }
     prices = append(prices, p) 
    }
    fmt.Println(prices) //打印每个p的内容
    .
    .
    .
    打印结果:
    p的Detail的Data的地址: 0xc0420420c0 
    p的Detail的Data的地址: 0xc042042120  //地址改变了
    [{2017-11-08 [{500 600 550} {500 600 550}]} {2017-11-09 [{250 750 500} {250 750 500}]}]   //结果正确了


后来又发现,其实还有个方法可以避免这个问题,就是在申明结构体的时候,不要把Detail定义为Slice,定义为一个空的interface就行,但是传的时候还是传切片,在Unmarshal的时候,golang发现这是个空接口,就会去给它申请新的地址,后来验证是没问题的,即:

type Spot struct {
    UpdateDate string `json:"update_date"`
    Detail interface{} `json:"detail"`
}

       之前自以为对slice很熟悉,也知道它的内部结构是会指向一个地址的,但是真正用起来还是要多加注意,一不小心就忽略了,引以为戒啊。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值