Шаблоны параллелизма Go: Context
На серверах Go каждый входящий запрос обрабатывается в собственной горутине. Обработчики запросов часто запускают дополнительные горутины для доступа к бэкэндам, таким как базы данных и службы RPC. Набору горутин, работающих над запросом, обычно требуется доступ к значениям, зависящим от запроса, таким как идентификация конечного пользователя, токены авторизации и крайний срок запроса. Когда запрос отменяется или истекает время ожидания, все горутины, работающие с этим запросом, должны быстро завершиться, чтобы система могла освободить любые ресурсы, которые они используют.
В Google мы разработали пакет context
, который упрощает передачу значений области запроса, сигналов отмены и крайних сроков через границы API всем горутинам, участвующим в обработке запроса. Пакет общедоступен как context. В этой статье описывается, как использовать пакет, и приводится полный рабочий пример.
Контекст
Ядро пакета context
- это тип Context
:
// Контекст передает крайний срок, сигнал отмены и значения области запроса
// через границы API. Его методы безопасны для одновременного использования
//
несколькими горутинами. type Context interface {
// Done возвращает канал, который закрывается, когда этот контекст отменяется
// или истекает время ожидания.
Done() <-chan struct{}
// Err указывает, почему этот контекст был отменен после
закрытия
// канала Done .
Err() error
// Крайний срок возвращает время, когда этот контекст будет отменен, если таковой имеется.
Deadline() (deadline time.Time, ok bool)
// Value возвращает значение, связанное с ключом, или nil, если его нет.
Value(key interface{}) interface{}
}
Метод Done
возвращает канал, который действует как сигнал аннулирования аккаунта функций, работающих от имени Context
: когда канал закрыт, функции должны отказаться от своей работы и возвращения. Метод Err
возвращает ошибку, указывающую, почему Context
было отменено.
Context
не имеет метода Cancel
по той же причине, по которой канал Done
является только приемным: функция, принимающая сигнал отмены, обычно не является той, которая посылает сигнал. В частности, когда родительская операция запускает горутины для подопераций, эти подоперации не должны иметь возможности отменить родительскую операцию. Вместо этого функция WithCancel
(описанная ниже) позволяет отменить новое значение Context
.
Context
безопасен для одновременного использования несколькими горутинами. Код может передать одиночный код Context
любому количеству горутин и отменить его Context
чтобы сигнализировать всем им.
Метод Deadline
позволяет функциям определить, следует ли им вообще начинать работу; если осталось слишком мало времени, это может оказаться нецелесообразным. Код также может использовать крайний срок для установки тайм-аутов для операций ввода-вывода.
Value
позволяет Context
переносить данные в пределах запроса. Эти данные должны быть безопасными для одновременного использования несколькими горутинами.
Производные контексты
Пакет context
предоставляет функции для Dérivé новых значений Context
из уже существующих. Эти значения образуют дерево: когда Context
отменяется, все производные Contexts
от него также отменяются.
Background
это корень любого дерева Context
; никогда не отменяется:
// Фон возвращает пустой контекст. Он никогда не отменяется, у него нет сроков
// и нет значений. Фон обычно используется в main, init и tests,
// а также в качестве контекста верхнего уровня для входящих запросов.
func Background() Context
WithCancel
и WithTimeout
возвращают производные значения Context
, которые можно отменить раньше, чем родительские Context
. Context
связанные с входящим запросом, как правило, отменяются, когда возвращается обработчик запросов. WithCancel
также полезно для отмены избыточных запросов при использовании нескольких реплик. WithTimeout
полезен для установки крайнего срока для запросов к внутренним серверам:
// WithCancel возвращает копию родительского элемента, канал Done которого закрывается, как только
// parent.Done закрывается или вызывается отмена.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// CancelFunc отменяет контекст.
type CancelFunc func()
// WithTimeout возвращает копию родительского элемента, канал Done которого закрывается, как только
// parent.Done закрывается, вызывается отмена или истекает тайм-аут.
//Крайний срок нового контекста - это более ранний из текущих + тайм-аут и крайний срок родителя,
// если есть. Если таймер все еще работает, функция отмены
// освобождает его ресурсы.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithValue
предоставляет собой способ связать значения в области запроса с Context
:
// WithValue возвращает копию родительского элемента, метод Value которого возвращает значение val для ключа.
func WithValue(parent Context, key interface{}, val interface{}) Context
Лучший способ увидеть, как использовать пакет context
- это проработанный пример.
Пример: веб-поиск Google
Наш пример - HTTP-сервер, который обрабатывает URL-адреса, например /search?q=golang&timeout=1s
, перенаправляя запрос "golang" в Google Web Search API и отображая результаты. Параметр timeout
указывает серверу отменить запрос после этого продолжительность истечет.
Код разделен на три пакета:
- server предоставляет
main
функцию и обработчик для/search
. - userip предоставляет функции для извлечения IP-адреса пользователя из запроса и связывания его с файлом
Context
. - Google предоставляет функцию
Search
для отправки запроса в Google.
Серверная программа
Программа сервера обрабатывает запросы, например /search?q=golang
, обслуживая первые несколько результатов поиска Google для golang
. Он регистрируется handleSearch
для обработки конечной точки /search
. Обработчик Context
создает начальный вызов ctx
и принимает меры к его отмене при возврате обработчика. Если запрос включает параметр URL timeout
, Context
автоматически отменяется по истечении тайм-аута:
func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx - это Контекст для этого обработчика. Вызов cancel закрывает
канал
// ctx.Done, который является сигналом отмены для запросов,
// запущенных этим обработчиком.
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// У запроса есть время ожидания, поэтому создайте контекст, который равен
/ / отменяется автоматически по истечении тайм-аута.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel() // Cancel ctx as soon as handleSearch returns.
Обработчик извлекает запрос из запроса и извлекает IP-адрес клиента, вызывая пакет userip
. IP-адрес клиента необходим для внутренних запросов, поэтому handleSearch
прикрепляет его к ctx
:
// Проверяем поисковый запрос.
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}
// Сохраняем IP-адрес пользователя в ctx для использования кодом в других пакетах.
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)
Обработчик вызовов google.Search
с ctx
и query
:
// Запускаем поиск Google и распечатываем результаты.
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)
Если поиск завершается успешно, обработчик отображает результаты:
if err: = resultsTemplate.Execute (w, struct {
Results google.Results
Timeout, Elapsed time.Duration
} {
Results: results,
Timeout: timeout,
Elapsed: elapsed,
}); err! = nil {
log.Print (err)
return
}
Userrip пакета
Пакет userip предоставляет функции для извлечения IP-адреса пользователя из запроса и связывания его с файлом Context
. Context
обеспечивает сопоставление "ключ-значение", в котором ключи и значения имеют тип interface{}
. Типы ключей должны поддерживать равенство, а значения должны быть безопасными для одновременного использования несколькими горутинами. Такие пакеты, как userip
скрывают детали этого сопоставления и предоставляют строго типизированный доступ к определенному значению Context
.
Чтобы избежать конфликтов ключей, userip
определяет неэкспортированный тип key
и использует значение этого типа в качестве контекстного ключа:
// Тип ключа не экспортируется, чтобы предотвратить конфликты с контекстными ключами,
//
определенными в других пакетах. введите ключ int
// userIPkey - это контекстный ключ для IP-адреса пользователя. Его нулевое
// значение произвольно. Если бы в этом пакете были определены другие ключи контекста, они
//
имели бы разные целочисленные значения.
const userIPKey key = 0
FromRequest
извлекает значение userIP
из http.Request
:
func FromRequest (req * http.Request) (net.IP, error) {
ip, _, err: = net.SplitHostPort (req.RemoteAddr)
if err! = nil {
return nil, fmt.Errorf ("userip:% q не IP: порт ", req.RemoteAddr)
}
NewContext
возвращает новый Context
который содержит предоставленное значение userIP
:
func NewContext (ctx context.Context, userIP net.IP) context.Context {
return context.WithValue (ctx, userIPKey, userIP)
}
FromContext
извлекает userIP
из Context
:
func FromContext(ctx context.Context) (net.IP, bool) {
// ctx.Value возвращает nil, если ctx не имеет значения для ключа;
// утверждение типа net.IP возвращает ok = false для nil.
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}
Пакет google
Функция google.Search отправляет HTTP-запрос к API веб-поиска Google и анализирует результат в формате JSON. Он принимает Context
параметр ctx
и немедленно возвращается, если ctx.Done
закрывается во время выполнения запроса.
Запрос API веб-поиска Google включает поисковый запрос и IP-адрес пользователя в качестве параметров запроса:
func Search(ctx context.Context, query string) (Results, error) {
// Готовим запрос Google Search API.
req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("q", query)
// Если ctx передает IP-адрес пользователя, перенаправить его на сервер.
// API Google используют IP-адрес пользователя, чтобы отличать запросы, инициированные сервером,
// от
запросов конечного пользователя.
if userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()
Search
использует вспомогательную функцию httpDo
для отправки HTTP-запроса и отмены его, если ctx.Done
закрыт во время обработки запроса или ответа. Search
передает закрытие для обработки HTTP-ответа httpDo
:
var results Results
err = httpDo(ctx, req, func(resp *http.Response, err error) error {
if err != nil {
return err
}
defer resp.Body.Close()
// Анализировать результат поиска JSON.
// https://developers.google.com/web-search/docs/#fonje
var data struct {
ResponseData struct {
Results []struct {
TitleNoFormatting string
URL string
}
}
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
for _, res := range data.ResponseData.Results {
results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
}
return nil
})
// httpDo ожидает возврата предоставленного нами закрытия, поэтому
// читать здесь результаты безопасно.
return results, err
Функцией httpDo
выполняется запрос HTTP и обрабатывает свой ответ в новой горутине. Она отменяет запрос, если ctx.Done
закрыт до выхода из горутины:
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// Выполните HTTP-запрос в горутине и передайте ответ f.
c := make(chan error, 1)
req = req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()
select {
case <-ctx.Done():
<-c // Дождитесь возврата f.
return ctx.Err()
case err := <-c:
return err
}
}
Адаптация кода для контекстов
Многие серверные инфраструктуры предоставляют пакеты и типы для переноса значений области запроса. Мы можем определить новые реализации интерфейса Context
для моста между кодом, использующим существующие фреймворки, и кодом, который ожидает параметр Context
.
Например, пакет Gorilla github.com/gorilla/context позволяет обработчикам связывать данные с входящими запросами, обеспечивая сопоставление HTTP-запросов с парами ключ-значение. В gorilla.go мы предоставляем реализацию Context
, метод Value
которой возвращает значения, связанные с конкретным HTTP-запросом в пакете Gorilla.
Другие пакеты предоставляют поддержку отмены, аналогичную Context
. Например, Tomb предоставляет метод Kill
, который сигнализирует об отмене, закрывая канал Dying
. Tomb
также предоставляет методы ожидания выхода этих горутин, аналогичные sync.WaitGroup
. В tomb.go мы предоставляем реализацию Context
, которая отменяется, когда отменяется его родительский объект Context
, либо объект предоставлен Tomb
.
Вывод
В Google мы требуем, чтобы программисты Go передавали параметр Context
в качестве первого аргумента каждой функции на пути вызова между входящими и исходящими запросами. Это позволяет коду Go, разработанному многими разными командами, хорошо взаимодействовать. Он обеспечивает простой контроль тайм-аутов и отмены и гарантирует, что критические значения, такие как учетные данные безопасности, должным образом передаются программами Go.
Фреймворки серверов, которые хотят развиваться Context
должны обеспечивать реализации моста Context
между своими пакетами и теми, которые ожидают параметр Context
. Тогда их клиентские библиотеки примут Context
от вызывающего кода. За счет создания общего интерфейса для данных в области запроса и отмены Context
разработчикам пакетов становится проще делиться кодом для создания масштабируемых служб.