1. 程式人生 > >寫golang restful介面時遇到的一個坑

寫golang restful介面時遇到的一個坑

話不多說,先上程式碼

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很熟悉,也知道它的內部結構是會指向一個地址的,但是真正用起來還是要多加註意,一不小心就忽略了,引以為戒啊。