Go 确保只检索一次值

Go 确保只检索一次值,go,concurrency,synchronisation,Go,Concurrency,Synchronisation,我正在开发一个Go包来访问web服务(通过HTTP)。每次我从该服务检索一页数据时,我也会得到可用页面的总数。得到这个总数的唯一方法是得到其中一页(通常是第一页)。但是,请求此服务需要时间,我需要执行以下操作: 当在客户机上调用GetPage方法并且第一次检索页面时,检索到的总计应存储在该客户机中的某个位置。调用Total方法时,如果尚未检索到总计,则应获取第一页并返回总计。如果之前通过调用GetPage或total检索到了总计,则应立即返回,而无需任何HTTP请求。这需要安全地供多个gorou

我正在开发一个Go包来访问web服务(通过HTTP)。每次我从该服务检索一页数据时,我也会得到可用页面的总数。得到这个总数的唯一方法是得到其中一页(通常是第一页)。但是,请求此服务需要时间,我需要执行以下操作:

当在
客户机上调用
GetPage
方法并且第一次检索页面时,检索到的总计应存储在该客户机中的某个位置。调用
Total
方法时,如果尚未检索到总计,则应获取第一页并返回总计。如果之前通过调用
GetPage
total
检索到了总计,则应立即返回,而无需任何HTTP请求。这需要安全地供多个goroutine使用。我的想法是类似于
sync的东西。一次
,但传递给
Do
的函数返回一个值,然后缓存该值,并在调用
Do
时自动返回

我记得以前看到过类似的东西,但现在即使我试过也找不到。搜索带有值和类似术语的
sync.Once
,没有得到任何有用的结果。我知道我可能可以使用互斥锁和大量锁定来实现这一点,但互斥锁和大量锁定似乎不是在go中实现这些功能的推荐方法。

General“init once”解决方案 在一般/通常情况下,只有在实际需要时才使用init一次的最简单解决方案是使用及其方法

实际上,您不需要从传递给
Once.Do()
的函数返回任何值,因为您可以将值存储到该函数中的全局变量等

请参见以下简单示例:

var (
    total         int
    calcTotalOnce sync.Once
)

func GetTotal() int {
    // Init / calc total once:
    calcTotalOnce.Do(func() {
        fmt.Println("Fetching total...")
        // Do some heavy work, make HTTP calls, whatever you want:
        total++ // This will set total to 1 (once and for all)
    })

    // Here you can safely use total:
    return total
}

func main() {
    fmt.Println(GetTotal())
    fmt.Println(GetTotal())
}
以上内容的输出(在上尝试):

一些注意事项:

  • 您可以使用互斥体或
    sync.Once
    实现同样的效果,但后者实际上比使用互斥体更快
  • 如果以前调用过
    GetTotal()
    ,则对
    GetTotal()
    的后续调用将只返回先前计算的值,这就是
    Once.do()
    所做的
    sync。一旦
    “跟踪”了它的
    Do()
    方法,如果以前调用过,则传递的函数值将不再被调用
  • sync.Once
    提供了此解决方案在多个goroutine中同时安全使用的所有需求,前提是您不直接从任何其他地方修改或访问
    total
    变量
你的“不寻常”案例的解决方案 一般情况下,假定只通过
GetTotal()
函数访问
total

在您的情况下,这不成立:您希望通过
GetTotal()
函数访问它,并且
GetPage()
调用后设置它(如果尚未设置)

我们也可以通过同步一次来解决这个问题。我们需要上面的
GetTotal()
函数;当执行
GetPage()
调用时,它可以使用相同的
calcTotalOnce
尝试从接收到的页面设置其值

它可能看起来像这样:

var (
    total         int
    calcTotalOnce sync.Once
)

func GetTotal() int {
    calcTotalOnce.Do(func() {
        // total is not yet initialized: get page and store total number
        page := getPageImpl()
        total = page.Total
    })

    // Here you can safely use total:
    return total
}

type Page struct {
    Total int
}

func GetPage() *Page {
    page := getPageImpl()

    calcTotalOnce.Do(func() {
        // total is not yet initialized, store the value we have:
        total = page.Total
    })

    return page
}

func getPageImpl() *Page {
    // Do HTTP call or whatever
    page := &Page{}
    // Set page.Total from the response body
    return page
}
这是怎么回事?我们在变量
calcTotalOnce
中创建并使用单个
sync.Once
。这确保了它的
Do()
方法只能调用传递给它的函数一次,无论在何处/如何调用该
Do()
方法

如果有人首先调用
GetTotal()
函数,那么它内部的函数文本将运行,调用
getPageImpl()
page.total
字段获取页面并初始化
total
变量

如果首先调用
GetPage()
函数,那么也将调用
calctotalone.Do()
,它只是将
Page.Total
值设置为
Total
变量

无论先走哪条路线,都会改变
calcTotalOnce
的内部状态,它将记住已运行的
total
计算,并且对
calcTotalOnce.Do()的进一步调用将永远不会调用传递给它的函数值

或者只使用“急切”初始化 还要注意的是,如果在程序的生命周期内必须获取这个总数,那么它可能不值得这么复杂,因为在创建变量时,您可以轻松地初始化变量一次

var Total = getPageImpl().Total
或者,如果初始化稍微复杂一些(例如需要错误处理),请使用包
init()
函数:

var Total int

func init() {
    page := getPageImpl()
    // Other logic, e.g. error handling
    Total = page.Total
}

如果你不认为互斥锁是围棋中做事情的方式,你应该。
sync.Once
是你通常确保某件事情只做一次的方式。您实际使用它时遇到了什么问题?如何检查值是否已提取?我知道我可以用枪,但这样安全吗?如果一个goroutine从那个bool读取,而另一个goroutine向它写入呢?你不需要检查。该检查由
一次完成。Do()
。如果调用了您的函数,则尚未对其进行设置。total可能已通过先前对GetPage的调用进行了设置,在这种情况下,GetTotal不应执行任何操作。我不认为同步。一次是这里的解决方案。只需使用互斥锁。@Peter互斥锁比使用
sync.one
慢。结果是一样的,我刚才才意识到你可以传递不同的函数去做。我一直认为在每个调用站点都必须传递相同的函数。现在说得通了,谢谢。
var Total int

func init() {
    page := getPageImpl()
    // Other logic, e.g. error handling
    Total = page.Total
}