- видео информация
- Самостоятельное введение
- зачем нужно
Context
-
Context
детали реализации - Суммировать
Context
- Q&A
видео информация
How to correctly use package context
by Jack Lindamood
at Golang UK Conf. 2017
- видео:Woohoo.YouTube.com/watch?V=-_B…
- Сообщение блога:medium.com/@evaluation21/how-…
Самостоятельное введение
Jack Lindamood Github: @cep21, Medium: @cep21, электронная почта: cep221 на gmail.com
- Пишу Go уже 4 года
- В настоящее время инженер-программист в Twitch.
- Основной бэкэнд Twitch написан на Go
- Существуют сотни репозиториев проектов
зачем нужноContext
- Каждыйдлинный запросдолжен иметьлимит тайм-аута
- Нужно пройти этот тайм-аут в вызове
- Например, когда мы начинаем обрабатывать запрос, мы говорим, что это 3-секундный тайм-аут.
- Итак, в середине вызова функции, сколько времени осталось для этого тайм-аута?
- Где эту информацию нужно хранить, чтобы обработка запроса могла остановиться на середине
Если подумать дальше.
Вызов RPC, как показано выше, начните вызовRPC 1
После этого внутри звонит соответственноRPC 2
, RPC 3
, RPC 4
и вернуть результат после успешного завершения всех вызовов RPC.
Это нормальный способ, но еслиRPC 2
Что происходит, когда вызов не удается?
RPC 2
После сбоя, если Контекста нет, мы все еще можем ждать выполнения всех RPC, но из-заRPC 2
Failed, так что на самом деле другие результаты RPC не имеют большого значения, нам все равно нужно возвращать пользователю ошибки. Поэтому мы зря потратили 10 мс, и нет необходимости ждать окончания выполнения других RPC.
то если мыRPC 2
После сбоя вернуть сбой непосредственно пользователю?
Пользователь получил сообщение об ошибке через 30 мс, ноRPC 3
а такжеRPC 4
Он по-прежнему работает бессмысленно и по-прежнему тратит вычислительные ресурсы и ресурсы ввода-вывода.
Таким образом, идеальное состояние должно быть таким, как показано выше, когдаRPC 2
После ошибки, в дополнение к возврату пользовательского сообщения об ошибке, у нас также должен быть какой-то способ уведомитьRPC 3
а такжеRPC 4
, остановите и их, и перестаньте тратить ресурсы впустую.
Итак, решение:
- Сигнал запроса на остановку
- Содержит некоторые подсказки о том, когда запрос может закончиться (тайм-аут).
- Используйте канал, чтобы уведомить об окончании запроса
Так что давайте просто бросим переменные туда же. 😈
- В Go нет рутинных переменных thread/go.
- На самом деле, это вполне разумно, потому что это сделает горутины зависимыми друг от друга.
- очень легко злоупотреблять
Context
детали реализации
context.Context
:
- является неизменным узлом дерева
- Отменить узел вместе с Отменить все его дочерние узлы (сверху донизу)
- Контекстные значения — это узел
- Поиск значения — это способ вернуться к дереву (снизу вверх)
ПримерContext
цепь
Полный код:play.golang.org/fear/DDP из BV1Q…
package main
func tree() {
ctx1 := context.Background()
ctx2, _ := context.WithCancel(ctx1)
ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
ctx6 := context.WithValue(ctx5, "userID", 12)
}
если так устроеноContext
цепь, ее форма следующая:
Затем, когда истекает 3-секундный тайм-аут:
можно увидетьctx4
Тайм-аут истек.
Когда наступает 5-секундный тайм-аут:
Видно, что не толькоctx3
вышел, все его дочерние узлы, такие какctx5
а такжеctx6
также бросить.
context.Context
API
В основном есть два типа операций:
- 3 функции используются дляОграничьте выход ваших дочерних узлов;
- 1 функция дляУстановите переменную области запроса
type Context interface {
// 啥时候退出
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
// 设置变量
Value(key interface{}) interface{}
}
когда его следует использоватьContext
?
- Каждый вызов RPC должен иметьтайм-аутспособность, которая является более разумным дизайном API
- не толькоэто тайм-аут, и вам также нужна возможность завершать действия, которые больше не требуют операций
-
context.Context
это стандартное решение Go - Любая функция, которая может блокироваться или выполняться долго, должна иметь
context.Context
Как создатьContext
?
- В начале RPC используйте
context.Background()
- некоторые люди ставят
main()
запись одинcontext.Background()
, а затем поместите это в переменную сервера, а затем наследуйте контекст от этой переменной, когда придет запрос. Этонеправильно. Направляйте каждый запрос, исходящий от его собственногоcontext.Background()
Вот и все.
- некоторые люди ставят
- Если у вас нет контекста, но вам нужно вызвать функцию контекста, используйте
context.TODO()
- Если для шага требуется собственная настройка тайм-аута, дайте ему отдельный подконтекст (как в предыдущем примере).
Как интегрироваться в API?
- Если есть Контекст,сделать это первой переменной.
- подобно
func (d* Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
- Некоторые люди помещают контекст в переменную посередине, что очень непривычно, не делайте этого, поместите его в первую.
- подобно
- сделать это какнеобязательныйпуть, с
request
структурный метод.- Такие как:
func (r *Request) WithContext(ctx context.Context) *Request
- Такие как:
- Пожалуйста, используйте имя переменной Context
ctx
(Не используйте странные имена 😓)
Context
куда?
- Пучок
Context
Подумайте о реке, протекающей через вашу программу (другое значение — не пить из реки... 🙊) - В идеале,
Context
Существовать в стеке вызовов (Call Stack) - не делай
Context
хранить вstruct
внутри- если вы не используете что-то вроде
http.Request
серединаrequest
способ структуры
- если вы не используете что-то вроде
-
request
Структура должна заканчиваться в конце запроса - Когда запрос RPC обрабатывается, ссылка на переменную Context должна быть удалена (Unreference).
- Когда Запрос заканчивается, Контекст должен закончиться. (Эти двое - пара. Они не хотят родиться в один и тот же день в одном и том же году, но они хотят умереть в том же году, в том же месяце и в тот же день…💕)
Context
Примечания к пакету
- Привыкайте закрывать контекст
- особенноКонтексты с истекшим сроком действия
- Если контекст подвергается сбору мусора, а не отменяется, вы обычно делаете это неправильно.
ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
defer cancel()
- Использование результатов тайм-аута для внутреннего использования
time.AfterFunc
, что приведет к тому, что контекст не будет собирать мусор, пока не истечет время таймера. - Сразу после установления
defer cancel()
это хорошая привычка.
Отмена запроса
Когда вас больше не волнуют результаты, полученные затем, можно ли отменить контекст?
кgolang.org/x/sync/errgroup
Например,errgroup
Используйте контекст, чтобы обеспечить поведение завершения RPC.
type Group struct {
cancel func()
wg sync.WaitGroup
errOnce sync.Once
err error
}
Создаватьgroup
а такжеcontext
:
func WithContext(ctx context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancel(ctx)
return &Group{cancel: cancel}, ctx
}
Это возвращает тот, который может быть отменен заранееgroup
.
При звонке напрямую не вызываетсяgo func()
, вместо вызоваGo()
, передать функцию в качестве параметра и вызвать ее в виде функции более высокого порядка, которая является внутреннейgo func()
Запустите горутину.
func (g *Group) Go(f func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := f(); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
}
})
}
}()
}
при задании функцииf
вернуть ошибку, использоватьsync.Once
отменитьcontext
, а ошибка сохраняется вg.err
среди последующихWait()
возврат в функции.
func (g *Group) Wait() error {
g.wg.Wait()
if g.cancel != nil {
g.cancel()
}
return g.err
}
Примечание: здесьWait()
После окончания позвоните один разcancel()
.
package main
func DoTwoRequestsAtOnce(ctx context.Context) error {
eg, egCtx := errgroup.WithContext(ctx)
var resp1, resp2 *http.Response
f := func(loc string, respIn **http.Response) func() error {
return func() error {
reqCtx, cancel := context.WithTimeout(egCtx, time.Second)
defer cancel()
req, _ := http.NewRequest("GET", loc, nil)
var err error
*resp, err = http.DefaultClient.Do(req.WithContext(reqCtx))
if err == nil && (*respIn).StatusCode >= 500 {
return errors.New("unexpected!")
}
return err
}
}
eg.Go(f("http://localhost:8080/fast_request", &resp1))
eg.Go(f("http://localhost:8080/slow_request", &resp2))
return eg.Wait()
}
В этом примере одновременно выполняются два вызова RPC, и когда истекает время ожидания вызова или возникает ошибка, другой вызов RPC завершается. Вот использование ранее упомянутогоerrgroup
Этот паттерн очень удобен, когда есть много не-запросов и нужно сосредоточиться на обработке тайм-аутов и завершении других параллельных задач с ошибками.
Context.Value
- значение категории Запрос
context.Value
клейкая лента API
С помощью клейкой ленты можно починить практически все: от разбитых коробок до человеческих ран, автомобильных двигателей и даже космического корабля НАСА «Аполлон-13» (Да! Правдивая история). Так что в западной культуре лента — вещь «универсальная». Боюсь, по-китайски ему больше подходит слово «панацея», которое может вылечить почти все: от головной боли, жара мозга, простуды и лихорадки до синяков.
Конечно,Лечите симптомы, но не причину, этот подтекст в восточной и западной культурах одинаков. упоминается здесьcontext.Value
Для API это что-то в этом роде, и все можно сделать, но это не лекарство от симптомов.
-
value
Узел — это узел в цепочке контекста.
package context
type valueCtx struct {
Context
key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
// ...
return &valueCtx{parent, key, val}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
можно увидеть,WithValue()
По сути, это просто добавление узла в структуру дерева контекста.
Контекст неизменен.
Ограничьте ключевое пространство
Чтобы предотвратить дублирование ключей в древовидной структуре, рекомендуется ограничить пространство ключей. Например, используя частный тип, затем используяGetXxx()
а такжеWithXxxx()
управлять частными лицами.
type privateCtxType string
var (
reqID = privateCtxType("req-id")
)
func GetRequestID(ctx context.Context) (int, bool) {
id, exists := ctx.Value(reqID).(int)
return id, exists
}
func WithRequestID(ctx context.Context, reqid int) context.Context {
return context.WithValue(ctx, reqID, reqid)
}
использовать здесьWithXxx
вместоSetXxx
Это также связано с тем, что контекст фактически неизменяем, поэтому вместо изменения значения в контексте создается новый контекст.со значением.
Context.Value
неизменен
Подчеркивайте снова и сноваContext.Value
даimmutableне слишком.
-
context.Context
По замыслу он устроен по неизменяемому (неизменяемому) шаблону - такой же,
Context.Value
также неизменен - не пытайся
Context.Value
Сохраните в нем изменяемое значение, а затем измените его, ожидая, что другие контексты увидят изменение.- не ожидай
Context.Value
В нем хранятся переменные значения, и, в конце концов, нет конкуренции или риска для одновременного доступа нескольких горутин, потому что от начала до конца он разработан в соответствии с неизменностью - Например, если установлен тайм-аут, не думайте, что вы можете изменить значение тайм-аута этого параметра.
- не ожидай
- В использовании
Context.Value
помни об этом, когда
что нужно положитьContext.Value
внутри?
- Должен содержать значение категории запроса
- Все, что касается самого Контекста, находится в категории «Запрос» (эти двое живут и умирают вместе).
- Получено из данных запроса и завершено запросом
Что не относится к категории запросов?
- Создается вне запроса и не изменяется вместе с запросом
- как ты
func main()
Встроенные вещи явно не из разряда Запросов
- как ты
- Подключение к базе данных
- если
User ID
в связи? (будет упомянуто позже)
- если
- Глобальный
logger
- если
logger
должен иметьUser ID
Шерстяная ткань? (будет упомянуто позже)
- если
затем используйтеContext.Value
В чем проблема?
- К сожалению, похоже, что все вытекает из запроса
- Так зачем нам все еще нужны параметры функции? Тогда просто приходите только с одним контекстом и все кончено?
func Add(ctx context.Context) int {
return ctx.Value("first").(int) + ctx.Value("second").(int)
}
Однажды я видел API, это форма:
func IsAdminUser(ctx context.Context) bool {
userID := GetUser(ctx)
return authSingleton.IsAdmin(userID)
}
Эта реализация API внутренне начинается сcontext
получено вUserID
, а затем выполнить оценку разрешений. Но по сигнатуре функции совершенно невозможно понять, что нужно этой функции и что она делает.
Код должен быть разработан так, чтобы в первую очередь он был удобочитаемым.
Когда другие получают код, они обычно не вникают в детали реализации функции и читают код построчно, а сначала просматривают интерфейс функции. Поэтому четкий дизайн интерфейса функций будет более выгодным для других (или себя через несколько месяцев), чтобы понять этот код.
Хороший дизайн API должен четко понимать логику функции из сигнатуры функции. Если мы изменим интерфейс выше на:
func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool
Мы можем ясно знать из этой сигнатуры функции:
- Эта функция, вероятно, будет отменена раньше
- Эта функция требует
User ID
- Эта функция требует
authenticator
Приходить - И потому что
authenticator
Это передача параметров вместо того, чтобы полагаться на что-то неявное Мы знаем, что легко передать фиктивную функцию аутентификации для тестирования при тестировании. -
userID
это входящее значение, поэтому мы можем изменить его, не беспокоясь о том, что это повлияет на другие вещи.
Вся эта информация получается из сигнатуры функции без необходимости открывать реализацию функции построчно.
Что вы можете положить вContext.Value
в?
знать сейчасContext.Value
Делает определение интерфейса более неясным и не должно казаться используемым. Итак, вернемся к первоначальному вопросу, что можно поместить вContext.Value
в? Подумайте об этом с другой стороны, что не является производным от Запроса?
-
Context.Value
Это должно быть что-то, что информирует, а не что-то, что контролирует - Никогда не нужно записывать в документ в качестве входных данных, которые должны существовать
- Если вы найдете свою функцию в каком-то
Context.Value
работает некорректно, значит этоContext.Value
Информация должна размещаться не внутри, а на интерфейсе. Потому что интерфейс сделали слишком расплывчатым.
Что не является контролирующей вещью?
- Request ID
- Просто дайте каждому вызову RPC идентификатор, никакого практического смысла
- Это число/строка, вы бы все равно не использовали ее как логическое суждение.
- Как правило, необходимо записывать журнал, когда
- а также
logger
сам не находится в категории «Запрос», поэтомуlogger
не должно бытьContext
внутри - Нет в категории Запрос
logger
следует просто использоватьContext
информация для украшения журнала
- а также
- ID пользователя (если только для логирования)
- Incoming Request ID
Что явно носит контролирующий характер?
- Подключение к базе данных
- Очевидно, это серьезно повлияет на логику.
- Поэтому это должно быть явно указано в параметре функции
- Служба аутентификации (Аутентификация)
- Очевидно, что разные сервисы аутентификации ведут к разной логике.
- Его также следует разместить в параметрах функции и четко указать.
пример
отладчикContext.Value
- net/http/httptrace
package main
func trace(req *http.Request, c *http.Client) {
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Println("Got Conn")
},
ConnectStart: func(network, addr string) {
fmt.Println("Dial Start")
},
ConnectDone: func(network, addr string, err error) {
fmt.Println("Dial done")
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
c.Do(req)
}
net/http
как пользоватьсяhttptrace
из?
- Если есть
trace
Если он существует, выполнитеtrace
Перезвоните - это толькоинформативный характер, вместохарактер контроля
-
http
не из-за существованияtrace
Существует ли другая логика выполнения - Это просто для информирования пользователей API, чтобы помочь пользователям записывать журналы или отлаживать
- так вот
trace
существует в контексте
-
package http
func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
// ...
trace := httptrace.ContextClientTrace(req.Context())
// ...
if trace != nil && trace.WroteHeaders != nil {
trace.WroteHeaders()
}
}
Избегайте внедрения зависимостей -github.com/golang/oauth2
- Здесь странно, используйте
ctx.Value
найти зависимости -
не рекомендуется
- Это в основном сделано здесь только для нужд тестирования
package main
import "github.com/golang/oauth2"
func oauth() {
c := &http.Client{Transport: &mockTransport{}}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
conf := &oauth2.Config{ /* ... */ }
conf.Exchange(ctx, "code")
}
злоупотребление людьмиContext.Value
причина
- Абстракция промежуточного программного обеспечения
- Очень глубокий стек вызовов функций
- беспорядочный дизайн
context.Value
Это не делает ваш API чище, это иллюзия, наоборот, это делает ваше определение API более неясным.
СуммироватьContext.Value
- Очень удобно для отладки
- Внесите необходимую информацию в
Context.Value
, сделает определение интерфейса более непрозрачным - Если это можно определить максимально четко в интерфейсе
- попробуй не использовать
Context.Value
СуммироватьContext
- Все длинные, блокирующие операции требуют
Context
-
errgroup
построен наContext
красивая абстракция сверху - Когда запрос завершен, Отмена
Context
-
Context.Value
следует использовать дляинформативный характервещи, а нехарактер контролявещи - ограничение
Context.Value
ключевое пространство -
Context
так же какContext.Value
Должен быть неизменным и должен быть потокобезопасным -
Context
должен следоватьRequest
погибнуть и погибнуть
Q&A
Доступ к базе данных также использует контекст?
Как упоминалось ранее, Context используется для долгосрочных блокирующих операций, и то же самое верно для операций с базой данных. Однако для операции Cancel по тайм-ауту операция записи обычно не отменяется, а для операции чтения обычно имеется операция Cancel.