Обеспечение получения значения только один раз - PullRequest
0 голосов
/ 04 мая 2018

Я разрабатываю пакет Go для доступа к веб-службе (через HTTP). Каждый раз, когда я получаю страницу данных из этого сервиса, я также получаю общее количество доступных страниц. Единственный способ получить эту сумму - получить одну из страниц (обычно первую). Однако запросы к этой службе занимают время, и мне нужно сделать следующее:

Когда метод GetPage вызывается для Client и страница извлекается впервые, извлеченная сумма должна храниться где-то в этом клиенте. Когда вызывается метод Total и итог еще не получен, должна быть выбрана первая страница и возвращена сумма. Если сумма была получена ранее, либо путем вызова GetPage или Total, она должна быть возвращена немедленно, без каких-либо HTTP-запросов вообще. Это должно быть безопасно для использования несколькими программами. Моя идея заключается в том, чтобы что-то вроде sync.Once, но с функцией, переданной Do, возвращающей значение, которое затем кэшируется и автоматически возвращается при вызове Do.

Я помню, что видел что-то подобное раньше, но я не могу найти это сейчас, хотя я пытался. Поиск sync.Once со значением и аналогичными терминами не дал никаких полезных результатов. Я знаю, что, вероятно, мог бы сделать это с мьютексом и большим количеством блокировок, но мьютексы и много блокировок не кажутся рекомендованным способом сделать что-то на ходу.

1 Ответ

0 голосов
/ 04 мая 2018

Общее решение "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
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...