Видеозаметки: как правильно использовать контекст Golang

Go

видео информация

How to correctly use package context
by Jack Lindamood
at Golang UK Conf. 2017

Самостоятельное введение

Jack Lindamood Github: @cep21, Medium: @cep21, электронная почта: cep221 на gmail.com

  • Пишу Go уже 4 года
  • В настоящее время инженер-программист в Twitch.
  • Основной бэкэнд Twitch написан на Go
  • Существуют сотни репозиториев проектов

зачем нужноContext

  • Каждыйдлинный запросдолжен иметьлимит тайм-аута
  • Нужно пройти этот тайм-аут в вызове
    • Например, когда мы начинаем обрабатывать запрос, мы говорим, что это 3-секундный тайм-аут.
    • Итак, в середине вызова функции, сколько времени осталось для этого тайм-аута?
    • Где эту информацию нужно хранить, чтобы обработка запроса могла остановиться на середине

Если подумать дальше.

rpc fails

Вызов RPC, как показано выше, начните вызовRPC 1После этого внутри звонит соответственноRPC 2, RPC 3, RPC 4и вернуть результат после успешного завершения всех вызовов RPC.

Это нормальный способ, но еслиRPC 2Что происходит, когда вызов не удается?

rpc fails

RPC 2После сбоя, если Контекста нет, мы все еще можем ждать выполнения всех RPC, но из-заRPC 2Failed, так что на самом деле другие результаты RPC не имеют большого значения, нам все равно нужно возвращать пользователю ошибки. Поэтому мы зря потратили 10 мс, и нет необходимости ждать окончания выполнения других RPC.

то если мыRPC 2После сбоя вернуть сбой непосредственно пользователю?

rpc fails

Пользователь получил сообщение об ошибке через 30 мс, ноRPC 3а такжеRPC 4Он по-прежнему работает бессмысленно и по-прежнему тратит вычислительные ресурсы и ресурсы ввода-вывода.

rpc fails

Таким образом, идеальное состояние должно быть таким, как показано выше, когда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цепь, ее форма следующая:

context chain

Затем, когда истекает 3-секундный тайм-аут:

context chain

можно увидетьctx4Тайм-аут истек.

Когда наступает 5-секундный тайм-аут:

context chain

Видно, что не только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
  • Пожалуйста, используйте имя переменной Contextctx(Не используйте странные имена 😓)

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.