我正在开发一个Go包来访问Web服务(通过HTTP(。每次从该服务检索一页数据时,我也会获得可用页面的总数。获得此总数的唯一方法是获取其中一个页面(通常是第一个页面(。但是,对此服务的请求需要时间,我需要执行以下操作:
在Client
上调用GetPage
方法并且首次检索页面时,检索到的总计应存储在该客户端中的某个位置。当调用Total
方法并且尚未检索总计时,应提取第一页并返回总计。如果之前通过调用GetPage
或Total
检索了总计,则应立即返回,根本不需要任何 HTTP 请求。这需要被多个 goroutines 安全使用。我的想法是类似于sync.Once
,但将函数传递给Do
返回一个值,然后缓存并在调用Do
时自动返回。
我记得以前见过这样的东西,但现在即使我尝试过也找不到它。搜索具有值和类似术语的sync.Once
没有产生任何有用的结果。我知道我可能可以通过互斥锁和大量锁定来做到这一点,但是互斥锁和大量锁定似乎不是在 go 中执行操作的推荐方法。
通用的"初始化一次"解决方案
在一般/通常的情况下,仅在实际需要时才初始化一次的最简单解决方案是使用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
提供了此解决方案的所有需求,以便从多个 goroutines 安全地并行使用,前提是您不会直接从其他任何地方修改或访问total
变量。
解决您的"不可用"案例
一般情况下,假设仅通过GetTotal()
函数访问total
。
在您的情况下,这不成立:您想通过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()
来获取页面并从Page.Total
字段中初始化total
变量。
如果首先调用GetPage()
函数,则还将调用calcTotalOnce.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
}