Обеспечение получения значения только один раз

Я разрабатываю пакет 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
}
Другие вопросы по тегам