golang 指標型別引起的神奇 bug

kenuo發表於2019-05-26

下面是使用的結構體介面抽象定義,其實就是將結構體存進一個 map裡。由於是讀寫都比較頻繁,我加了讀寫鎖。

// add progress listener.
func (upload *UploaderGateway) AddProgress(key string, v ProgressListener) {
    upload.mutex.Lock()
    defer upload.mutex.Unlock()
    upload.ProgressMap[key] = v
}

//get progress listener.
func (upload *UploaderGateway) GetProgress(key string) (v ProgressListener, err error) {
    upload.mutex.RLock()
    defer upload.mutex.RUnlock()
    progressListener, ok := upload.ProgressMap[key]
    if !ok {
        return nil, errors.New("Get ProgressListener Not Found")
    }
    listener := progressListener.GetFormat()
    return &listener, nil
}

//delete progress listener.
func (upload *UploaderGateway) DeleteProgress(key string) {
    upload.mutex.Lock()
    defer upload.mutex.Unlock()
    delete(upload.ProgressMap, key)
}

結構體定義

// 定義進度條監聽器。
type OssProgressListener struct {
    FileSha1      string `json:"file_sha1"`      //file sha1
    FileSize      int64  `json:"file_size"`      //file size
    ConsumedBytes int64  `json:"consumed_bytes"` //consumed bytes
    mutex         *sync.RWMutex
}

// set consumed bytes.
func (listener *OssProgressListener) SetConsumedBytes(value int64) ProgressListener {
    listener.mutex.Lock()
    defer listener.mutex.Unlock()
    listener.ConsumedBytes = value
    return listener
}

// 定義進度變更事件處理函式。
func (listener *OssProgressListener) ProgressChanged(event *oss.ProgressEvent) {
    listener.SetConsumedBytes(event.ConsumedBytes)
    //pretty.Printf("event: %# v\n", event)
    //pretty.Printf("listener: %# v\n", listener)
    switch event.EventType {
    case oss.TransferStartedEvent:
        fmt.Printf("傳輸已啟動,已用位元組數: %d, 總計位元組: %d.\n",
            event.ConsumedBytes, listener.FileSize)
    case oss.TransferDataEvent:
        //if event.ConsumedBytes == 0 || listener.FileSize == 0 {
        //  fmt.Printf("傳輸資料,消耗位元組: %d, 總計位元組: %d, %d%%.\n",
        //      event.ConsumedBytes, listener.FileSize, event.ConsumedBytes)
        //} else {
        //  fmt.Printf("傳輸資料,消耗位元組: %d, 總計位元組: %d, %d%%.\n",
        //      event.ConsumedBytes, listener.FileSize, event.ConsumedBytes*100/listener.FileSize)
        //}
    case oss.TransferCompletedEvent:
        fmt.Printf("\n傳輸已完成,已用位元組數: %d, 總計位元組: %d.\n",
            event.ConsumedBytes, listener.FileSize)
    case oss.TransferFailedEvent:
        fmt.Printf("\n傳輸失敗,已用位元組數: %d, 總計位元組: %d.\n",
            event.ConsumedBytes, listener.FileSize)
    default:
    }
}

上面的 ProgressChanged 函式是個回撥函式,上傳進度會實時呼叫,然後去更新ConsumedBytes 的值.

那麼問題來了。當我呼叫 GetProgress 的時候,就會把將 OssProgressListener 結構體資訊返回出去,由於是api形式,框架底層會將結構體解析成json然後返回給瀏覽器。 那麼解析json的時候底層還是會去讀取這個結構體的值資訊。造成讀寫併發的問題。

解決思路

首先實現抽象一個結構返回這個結構體資訊加鎖,因為我們寫資料 ConsumedBytes 也使用到了鎖機制。

image.png

完整 Progress定義

/**
* Author: JeffreyBool
* Date: 2019/5/25
* Time: 19:13
* Software: GoLand
*/

package oss

import (
    "github.com/aliyun/aliyun-oss-go-sdk/oss"
    "fmt"
    "sync"
    "encoding/json"
)

type ProgressListener interface {
    oss.ProgressListener
    SetFileSha1(string) ProgressListener
    SetFileSize(int64) ProgressListener
    SetConsumedBytes(int64) ProgressListener
    GetFormat() OssProgressListener
}

// 定義進度條監聽器。
type OssProgressListener struct {
    FileSha1      string `json:"file_sha1"`      //file sha1
    FileSize      int64  `json:"file_size"`      //file size
    ConsumedBytes int64  `json:"consumed_bytes"` //consumed bytes
    mutex         *sync.RWMutex
}

//初始化進度條監聽器
func NewOssProgressListener() ProgressListener {
    return &OssProgressListener{mutex: new(sync.RWMutex)}
}

// set file sha1.
func (listener *OssProgressListener) SetFileSha1(value string) ProgressListener {
    listener.mutex.Lock()
    defer listener.mutex.Unlock()
    listener.FileSha1 = value
    return listener
}

// set file size.
func (listener *OssProgressListener) SetFileSize(value int64) ProgressListener {
    listener.mutex.Lock()
    defer listener.mutex.Unlock()
    listener.FileSize = value
    return listener
}

// set consumed bytes.
func (listener *OssProgressListener) SetConsumedBytes(value int64) ProgressListener {
    listener.mutex.Lock()
    defer listener.mutex.Unlock()
    listener.ConsumedBytes = value
    return listener
}

//獲取資料
//只能為了防止 json 序列化再次讀取這個值和寫衝突,就使用值拷貝的方式。
func (listener *OssProgressListener) GetFormat() OssProgressListener {
    listener.mutex.RLock()
    defer listener.mutex.RUnlock()
    //bytes, _ := listener.Marshal()
    return *listener
}

//json 序列化加鎖..防止資料衝突
func (listener *OssProgressListener) Marshal() ([]byte, error) {
    listener.mutex.RLock()
    defer listener.mutex.RUnlock()
    return json.Marshal(listener)
}

// 定義進度變更事件處理函式。
func (listener *OssProgressListener) ProgressChanged(event *oss.ProgressEvent) {
    listener.SetConsumedBytes(event.ConsumedBytes)
    //pretty.Printf("event: %# v\n", event)
    //pretty.Printf("listener: %# v\n", listener)
    switch event.EventType {
    case oss.TransferStartedEvent:
        fmt.Printf("傳輸已啟動,已用位元組數: %d, 總計位元組: %d.\n",
            event.ConsumedBytes, listener.FileSize)
    case oss.TransferDataEvent:
        //if event.ConsumedBytes == 0 || listener.FileSize == 0 {
        //  fmt.Printf("傳輸資料,消耗位元組: %d, 總計位元組: %d, %d%%.\n",
        //      event.ConsumedBytes, listener.FileSize, event.ConsumedBytes)
        //} else {
        //  fmt.Printf("傳輸資料,消耗位元組: %d, 總計位元組: %d, %d%%.\n",
        //      event.ConsumedBytes, listener.FileSize, event.ConsumedBytes*100/listener.FileSize)
        //}
    case oss.TransferCompletedEvent:
        fmt.Printf("\n傳輸已完成,已用位元組數: %d, 總計位元組: %d.\n",
            event.ConsumedBytes, listener.FileSize)
    case oss.TransferFailedEvent:
        fmt.Printf("\n傳輸失敗,已用位元組數: %d, 總計位元組: %d.\n",
            event.ConsumedBytes, listener.FileSize)
    default:
    }
}

上面的 GetFormat 就是我們對外暴露的資訊。 注意 讀取的時候 加鎖.然後我們需要返回這個結構體的值傳遞型別,一定不要返回指標型別。預設值傳遞型別會將資料拷貝一份返回出去,這樣外面拿到的資料就不是同一個變數地址的資料啦。這樣做 json 解析的時候就不會發生資料衝突的問題了。

image.png

資料衝突

image.png

上圖就是造成資料衝突的原因.

需要檢視資料衝突命令 -race

go run -race main.go

致謝

感謝你花時間閱讀,如果覺得作者寫的可以,可以將本站分享給更多的人。寫的不好別噴哈,小弟水平有限~~ ???

原文連結:https://www.zhanggaoyuan.com/article/18

原文標題:[golang指標型別引起的神奇bug]

本站使用「 署名-非商業性使用 4.0 國際 (CC BY-NC 4.0)」創作共享協議,轉載或使用請署名並註明出處。

by JeffreyBool blog :point_right: link

相關文章