为了账号安全,请及时绑定邮箱和手机立即绑定

在 Golang 中测试一次条件

在 Golang 中测试一次条件

Go
浮云间 2022-05-23 18:02:23
我有一个模块,它依赖于通过调用外部服务来填充缓存,如下所示:func (provider *Cache) GetItem(productId string, skuId string, itemType string) (*Item, error) {    // First, create the key we'll use to uniquely identify the item    key := fmt.Sprintf("%s:%s", productId, skuId)    // Now, attempt to get the concurrency control associated with the item key    // If we couldn't find it then create one and add it to the map    var once *sync.Once    if entry, ok := provider.lockMap.Load(key); ok {        once = entry.(*sync.Once)    } else {        once = &sync.Once{}        provider.lockMap.Store(key, once)    }    // Now, use the concurrency control to attempt to request the item    // but only once. Channel any errors that occur    cErr := make(chan error, 1)    once.Do(func() {        // We didn't find the item in the cache so we'll have to get it from the partner-center        item, err := provider.client.GetItem(productId, skuId)        if err != nil {            cErr <- err            return        }        // Add the item to the cache        provider.cache.Store(key, &item)    })    // Attempt to read an error from the channel; if we get one then return it    // Otherwise, pull the item out of the cache. We have to use the select here because this is    // the only way to attempt to read from a channel without it blocking    var sku interface{}    select {    case err, ok := <-cErr:        if ok {            return nil, err        }    default:        item, _ = provider.cache.Load(key)    }    // Now, pull out a reference to the item and return it    return item.(*Item), nil}这种方法按我的预期工作。我的问题是测试;专门测试以确保该GetItem方法仅针对给定的键值调用一次。我的测试代码如下:var _ = Describe("Item Tests", func() {    It("GetItem - Not cached, two concurrent requests - Client called once", func() {        // setup cache    })})我遇到的问题是,有时这个测试会失败,因为请求计数是 2,而在注释行中预期它是 1。此故障不一致,我不太确定如何调试此问题。任何帮助将不胜感激。
查看完整描述

1 回答

?
蝴蝶不菲

TA贡献1810条经验 获得超4个赞

我认为您的测试有时会失败,因为您的缓存无法保证它只获取一次项目,而且您很幸运测试抓住了这一点。


如果一个 item 不在其中,并且 2 个并发 goroutines 同时调用Cache.GetItem(),可能lockMap.Load()会在两者中报告 key 不在 map 中,两个 goroutines 都创建一个sync.Once,并且都将自己的实例存储在 map 中(显然只有一个——后者——将保留在地图中,但您的缓存不会检查这一点)。


然后 2 个 goroutine 都会调用client.GetItem(),因为 2 个单独sync.Once的不提供同步。只有使用相同的sync.Once实例,才能保证传递给的函数Once.Do()只执行一次。


我认为sync.Mutex避免sync.Once在这里创建和使用 2 会更容易、更合适。


或者由于您已经在使用sync.Map,您可以使用以下Map.LoadOrStore()方法:创建一个sync.Once,并将其传递给Map.LoadOrStore(). 如果键已经在地图中,请使用返回的sync.Once. 如果密钥不在地图中,您sync.Once将存储在其中,因此您可以使用它。这将确保没有多个并发 goroutine 可以sync.once在其中存储多个实例。


像这样的东西:


var once *sync.Once

if entry, loaded := provider.lockMap.LoadOrStore(key, once); loaded {

    // Was already in the map, use the loaded Once

    once = entry.(*sync.Once)

}

这个解决方案仍然不完美:如果 2 个 goroutineCache.GetItem()同时调用,只有一个会尝试从客户端获取项目,但如果失败,只有这个 goroutine 会报告错误,另一个 goroutine 不会尝试获取来自客户端的项目,但将从地图加载它并且您不检查加载是否成功。你应该,如果它不在地图中,这意味着另一个并发尝试未能获得它。所以你应该报告错误(并清除sync.Once)。


如您所见,它变得越来越复杂。我坚持我之前的建议:sync.Mutex在这里使用 a 会更容易。


查看完整回答
反对 回复 2022-05-23
  • 1 回答
  • 0 关注
  • 138 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信