一般的“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
“tracks”如果它的Do()
方法之前被调用过,如果是这样,传递的函数值将不再被调用。
sync.Once
鉴于您不total
直接从其他任何地方修改或访问变量,该解决方案提供了该解决方案的所有需求,可以安全地从多个 goroutine 并发使用。
解决您的“不寻常”案例
一般情况假设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()
将永远不会调用传递给它的函数值。
或者只是使用“渴望”初始化
另请注意,如果可能必须在程序的生命周期内获取此总数,则可能不值得上述复杂性,因为您可以在创建变量时轻松地初始化一次变量。
var Total = getPageImpl().Total
或者如果初始化稍微复杂一点(例如需要错误处理),请使用包init()
函数:
var Total int
func init() {
page := getPageImpl()
// Other logic, e.g. error handling
Total = page.Total
}