Обеспечение получения значения только один раз
Я разрабатываю пакет Go для доступа к веб-сервису (через HTTP). Каждый раз, когда я получаю страницу данных из этого сервиса, я также получаю общее количество доступных страниц. Единственный способ получить эту сумму - получить одну из страниц (обычно первую). Однако запросы к этому сервису занимают время, и мне нужно сделать следующее:
Когда GetPage
метод вызывается на Client
и страница извлекается впервые, полученная сумма должна храниться где-то в этом клиенте. Когда Total
вызывается метод, а итоговое значение еще не получено, следует извлечь первую страницу и вернуть итоговое значение. Если сумма была получена ранее, либо путем вызова GetPage
или же Total
, он должен быть возвращен немедленно, без каких-либо HTTP-запросов. Это должно быть безопасно для использования несколькими программами. Моя идея что-то вроде sync.Once
но с функцией, переданной Do
возвращая значение, которое затем кэшируется и автоматически возвращается всякий раз, когда Do
называется.
Я помню, что видел что-то подобное раньше, но я не могу найти это сейчас, хотя я пытался. В поисках sync.Once
с ценностями и аналогичными терминами не дали никаких полезных результатов. Я знаю, что, вероятно, мог бы сделать это с мьютексом и большим количеством блокировок, но мьютексы и много блокировок не кажутся рекомендуемым способом сделать что-то на ходу.
1 ответ
Общее решение "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
обеспечивает все потребности для того, чтобы это решение было безопасным для одновременного использования несколькими программами, учитывая, что вы не изменяете или не получаете доступ к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()
никогда не вызовет переданное ему значение функции.
Или просто используйте "нетерпеливую" инициализацию
Также обратите внимание, что если существует вероятность того, что это общее число должно быть получено в течение жизненного цикла вашей программы, оно может не стоить описанной выше сложности, так как вы можете так же легко инициализировать переменную один раз, когда она будет создана.
var Total = getPageImpl().Total
Или, если инициализация немного сложнее (например, требуется обработка ошибок), используйте пакет init()
функция:
var Total int
func init() {
page := getPageImpl()
// Other logic, e.g. error handling
Total = page.Total
}