我正在开发一个Go包以访问Web服务(通过HTTP)。每当我从该服务检索一页数据时,我也会获得可用的页面总数。获得此总数的唯一方法是获取其中一个页面(通常是第一个页面)。但是,对此服务的请求需要时间,我需要执行以下操作:
在GetPage
上调用Client
方法并且首次检索页面时,检索到的总数应该存储在该客户端的某个位置。调用Total
方法并且尚未检索到总计时,应提取第一页并返回总计。如果通过调用GetPage
或Total
之前检索到总数,则应立即返回,而不需要任何HTTP请求。这需要多个goroutine安全使用。我的想法与sync.Once
类似,但传递给Do
的函数返回一个值,然后缓存并在调用Do
时自动返回。
我记得以前见过这样的东西,但即使我尝试过,我现在也找不到它。搜索具有值和类似术语的sync.Once
并未产生任何有用的结果。我知道我可以使用互斥锁和大量锁定来实现这一点,但是互斥锁和大量锁定似乎不是推荐的方法。
答案 0 :(得分:2)
在一般/通常情况下,只有init一次的最简单的解决方案,只有在实际需要时才使用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())
}
。该函数中的全局变量。
见这个简单的例子:
Fetching total...
1
1
上述输出(在Go Playground上尝试):
sync.Once
一些注意事项:
GetTotal()
来实现相同功能,但后者实际上比使用互斥锁更快。GetTotal()
,则对Once.Do()
的后续调用将不会执行任何操作,只会返回先前计算的值,这是sync.Once
执行/确保的操作。 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()
变量} field。
如果首先调用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
}
函数:
claimRef