我正在开发一个 Go 包来访问 Web 服务(通过 HTTP)。每次我从该服务检索一页数据时,我也会获得可用页面的总数。获得此总数的唯一方法是获取其中一页(通常是第一页)。但是,对该服务的请求需要时间,我需要执行以下操作:
当 GetPage
方法在 Client
上调用并且第一次检索该页面,检索到的总数应存储在该客户端的某个位置。当 Total
方法被调用并且尚未检索总计,应获取第一页并返回总计。如果之前检索过总数,则可以通过调用 GetPage
或Total
,它应该立即返回,根本不需要任何 HTTP 请求。这需要能够安全地供多个 goroutine 使用。我的想法类似于 sync.Once
但将函数传递给 Do
返回一个值,然后将其缓存并在 Do
时自动返回被调用。
我记得以前见过类似的东西,但现在即使我尝试了也找不到它。正在寻找 sync.Once
具有值(value)和类似的术语没有产生任何有用的结果。我知道我可能可以使用互斥锁和大量锁定来做到这一点,但是互斥锁和大量锁定似乎并不是在 go 中执行操作的推荐方法。
最佳答案
通用“init-once”解决方案
在一般情况下,仅在实际需要时才初始化一次的最简单解决方案是使用 sync.Once
及其 Once.Do()
方法。
实际上,您不需要从传递给 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())
}
上述输出(在 Go Playground 上尝试):
Fetching total...
1
1
一些注意事项:
- 您可以使用互斥体或
sync.Once
来实现相同的目的,但后者实际上比使用互斥体更快。 - 如果
GetTotal()
之前已调用过,后续调用GetTotal()
不会做任何事情,只是返回之前计算的值,这就是Once.Do()
确实/确保。sync.Once
“跟踪”如果是Do()
方法之前已经被调用过,如果是,则传递的函数值将不再被调用。 -
sync.Once
提供了此解决方案的所有需求,以安全地从多个 goroutine 并发使用,前提是您不修改或访问total
直接从其他地方获取变量。
“不寻常”案例的解决方案
一般情况假设 total
只能通过 GetTotal()
访问功能。
在您的情况下,这不成立:您想通过 GetTotal()
访问它函数并且你想在 GetPage()
之后设置它调用(如果尚未设置)。
我们可以用 sync.Once
来解决这个问题也。我们需要上面的 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
}
这是如何工作的?我们创建并使用单个sync.Once
在变量calcTotalOnce
中。这确保了其 Do()
方法只能调用传递给它的函数一次,无论在哪里/如何 Do()
方法被调用。
如果有人调用GetTotal()
首先运行函数,然后运行其中的函数文字,调用 getPageImpl()
获取页面并初始化 total
来自 Page.Total
的变量字段。
如果GetPage()
函数将首先被调用,它也会调用 calcTotalOnce.Do()
它只是设置 Page.Total
值 total
变量。
无论先走哪条路线,都会改变 calcTotalOnce
的内部状态,它将记住 total
计算已经运行,进一步调用calcTotalOnce.Do()
永远不会调用传递给它的函数值。
或者只是使用“eager”初始化
另请注意,如果可能必须在程序的生命周期内获取此总数,则可能不值得如此复杂,因为您可以在创建变量时轻松地初始化该变量一次。
var Total = getPageImpl().Total
或者,如果初始化稍微复杂一点(例如需要错误处理),请使用包 init()
功能:
var Total int
func init() {
page := getPageImpl()
// Other logic, e.g. error handling
Total = page.Total
}
关于go - 确保值仅被检索一次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50182009/