Как правильно использовать Context в Golang

Go
Как правильно использовать Context в Golang

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

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

видео:Woohoo.YouTube.com/watch?V=-_B…Сообщение блога:medium.com/@evaluation21/how-…

Зачем нужен контекст

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

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

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

Это обычный способ, но что произойдет, если вызов RPC 2 завершится ошибкой?

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

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

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

Таким образом, идеальное состояние должно быть таким, как показано выше: при сбое RPC 2, в дополнение к возврату информации об ошибке пользователя, у нас также должен быть какой-то способ уведомить RPC 3 и RPC 4, чтобы они также прекратили работу без траты ресурсов.

Итак, решение:

  • Сигнал запроса на остановку
  • Содержит некоторые подсказки о том, когда запрос может закончиться (тайм-аут).
  • Используйте канал, чтобы уведомить об окончании запроса

Так что давайте просто бросим переменные туда же. 😈

  • В Go нет рутинных переменных thread/go.
    • На самом деле, это вполне разумно, потому что это сделает горутины зависимыми друг от друга.
  • очень легко злоупотреблять

Детали реализации контекста

контекст.Контекст:

  • является неизменным узлом дерева
  • Отменить узел вместе с Отменить все его дочерние узлы (сверху вниз)
  • Контекстные значения — это узел
  • Поиск значения — это способ вернуться к дереву (снизу вверх)

Пример цепочки контекстов

Полный код: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{}
}

Когда следует использовать контекст?

  • Каждый вызов RPC должен иметьтайм-аутспособность, которая является более разумным дизайном API
  • не толькоэто тайм-аут, и вам также нужна возможность завершать действия, которые больше не требуют операций
  • context.Context — это стандартное решение Go.
  • Любая функция, которая может блокироваться или выполняться долго, должна иметь контекст.Контекст

Как создать контекст?

  • В начале RPC используйте context.Background()
    • Некоторые люди записывают context.Background() в main(), затем помещают его в переменную сервера, а затем наследуют контекст от этой переменной при поступлении запроса. Этонеправильный. Непосредственно каждый запрос может быть получен из своего собственного context.Background() .
  • Если у вас нет контекста, но вам нужно вызвать функцию контекста, используйте context.TODO()
  • Если для шага требуется собственная настройка тайм-аута, дайте ему отдельный подконтекст (как в предыдущем примере).

Как интегрироваться в API?

  • Если есть Контекст,сделать это первой переменной.
    • Например, func (d* Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
    • Некоторые люди помещают контекст в переменную посередине, что очень непривычно, не делайте этого, поместите его в первую.
  • сделать это какпо желаниюметод, используя метод структуры запроса.
    • Например: func (r *Request) WithContext(ctx context.Context) *Request
  • Пожалуйста, используйте ctx для имени переменной Context (не используйте странные имена 😓)

Где Контекст?

  • Думайте о Контексте как о реке, протекающей через вашу программу (другое значение — не пить из реки... 🙊)
  • В идеале Context существует в стеке вызовов (Call Stack).
  • Не храните контекст в структуре
    • Если вы не используете что-то вроде структуры запроса в http.Request
  • Структура запроса должна заканчиваться в конце запроса.
  • Когда обработка запроса RPC завершена, вы должны удалить ссылку на переменную контекста (Unweference)
  • Когда Запрос заканчивается, Контекст должен закончиться. (Эти двое - пара. Они не хотят родиться в один и тот же день в одном и том же году, но они хотят умереть в том же году, в том же месяце и в тот же день…💕)

Примечания к пакету Context

  • Привыкайте закрывать контекст
    • особенноКонтексты с истекшим сроком действия
  • Если контекст подвергается сбору мусора, а не отменяется, вы обычно делаете это неправильно.
ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
defer cancel()
  • Использование Timeout приводит к внутреннему использованию time.AfterFunc, что приводит к тому, что контекст не будет собирать мусор, пока не истечет время таймера.
  • Хорошей практикой является отложенная процедура cancel() сразу после создания.

Отмена запроса

Когда вас больше не волнуют результаты, полученные затем, можно ли отменить контекст?

Возьмите golang.org/x/sync/errgroup в качестве примера, errgroup использует Context для обеспечения поведения завершения RPC.

type Group struct {
	cancel  func()
	wg      sync.WaitGroup
	errOnce sync.Once
	err     error
}

Создайте группу и контекст:

func WithContext(ctx context.Context) (*Group, context.Context) {
  ctx, cancel := context.WithCancel(ctx)
  return &Group{cancel: cancel}, ctx
}

Это возвращает группу, которую можно отменить раньше.

При вызове он не вызывает 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 для отмены контекста, и ошибка сохраняется в g.err и возвращается в последующей функции Wait().

func (g *Group) Wait() error {
  g.wg.Wait()
  if g.cancel != nil {
    g.cancel()
  }
  return g.err
}

Примечание: здесь, после завершения Wait(), отмена() вызывается один раз.

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
      *respIn, 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, и он может делать что угодно, но не лечит симптомы.

  • Узел значения — это узел в цепочке контекста.
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 на самом деле неизменяем, поэтому вместо изменения значения в Context создается новый Context.со значением.

Context.Value является неизменным

Опять же, Context.Valueimmutableне очень много.

  • context.Context предназначен для следования неизменному (неизменяемому) шаблону
  • Аналогично, Context.Value является неизменным.
  • Не пытайтесь сохранить изменяемое значение в Context.Value, а затем изменить его, ожидая, что другие контексты увидят изменение.
    • Не ожидайте хранить изменяемые значения в Context.Value, В конце концов, одновременный доступ нескольких горутин не имеет конкуренции или риска, потому что от начала до конца он разработан, чтобы быть неизменным.
    • Например, если установлен тайм-аут, не думайте, что вы можете изменить значение тайм-аута этого параметра.
  • Всегда помните об этом при использовании Context.Value.

Что я должен положить в Context.value?

  • Должен содержать значение категории запроса
    • Все, что касается самого Контекста, находится в категории «Запрос» (эти двое живут и умирают вместе).
    • Получено из данных запроса и завершено запросом

Что не относится к категории запросов?

  • Создается вне запроса и не изменяется вместе с запросом
    • Например, то, что вы создаете в func main(), явно не относится к категории Request.
  • Подключение к базе данных
    • Что делать, если идентификатор пользователя находится в соединении? (будет упомянуто позже)
  • глобальный регистратор
    • Что делать, если в регистраторе требуется идентификатор пользователя? (будет упомянуто позже)

Так что же не так с использованием 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 получает идентификатор пользователя из контекста внутренне, а затем выполняет оценку разрешений. Но по сигнатуре функции совершенно невозможно понять, что нужно этой функции и что она делает.

Код должен быть разработан так, чтобы в первую очередь он был удобочитаемым.

Когда другие получают код, они обычно не вникают в детали реализации функции и читают код построчно, а сначала просматривают интерфейс функции. Поэтому четкий дизайн интерфейса функций будет более выгодным для других (или себя через несколько месяцев), чтобы понять этот код.

Хороший дизайн API должен четко понимать логику функции из сигнатуры функции. Если мы изменим интерфейс выше на:

func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool

Мы можем ясно знать из этой сигнатуры функции:

  • Эта функция, вероятно, будет отменена раньше
  • Для этой функции требуется идентификатор пользователя
  • Эта функция требует, чтобы аутентификатор
  • И поскольку аутентификатор является входящим параметром, а не полагаться на что-то неявное, мы знаем, что легко передать фиктивную функцию аутентификации для тестирования при тестировании.
  • userID — это входящее значение, поэтому мы можем изменить его, не беспокоясь о других вещах.

Вся эта информация получается из сигнатуры функции без необходимости открывать реализацию функции построчно.

Так что же можно поместить в Context.Value?

Теперь, зная, что Context.Value сделает определение интерфейса более двусмысленным, кажется, что его не следует использовать. Итак, вернемся к исходному вопросу: что можно поместить в Context.Value? Подумайте об этом с другой стороны, что не является производным от Запроса?

  • Context.Value должен информировать, а не контролировать
  • Никогда не нужно записывать в документ в качестве входных данных, которые должны существовать
  • Если вы обнаружите, что ваша функция работает некорректно под каким-то Context.Value, это значит, что информацию в этом Context.Value нужно размещать не в нем, а на интерфейсе. Потому что интерфейс сделали слишком расплывчатым.

Что не является контролирующей вещью?

  • Request ID
    • Просто дайте каждому вызову RPC идентификатор, никакого практического смысла
    • Это число/строка, вы бы все равно не использовали ее в качестве логического суждения.
    • Как правило, необходимо записывать журнал, когда
      • Сам логгер не находится в категории Запрос, поэтому логгер не должен быть в Контексте
      • Регистраторы, не входящие в категорию «Запрос», должны использовать только контекстную информацию для украшения журнала.
  • ID пользователя (если только для логирования)
  • Incoming Request ID

Что явно носит контролирующий характер?

  • Подключение к базе данных
    • Очевидно, это серьезно повлияет на логику.
    • Поэтому это должно быть явно указано в параметре функции
  • Служба аутентификации (Аутентификация)
    • Очевидно, что разные сервисы аутентификации ведут к разной логике.
    • Его также следует разместить в параметрах функции и четко указать.

пример

Context.Value отладочного характера - net/http/httptrace

medium.com/@evaluation21/go-1…

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)
}

Как сеть/http использует httptrace?

  • Если трассировка существует, выполните функцию обратного вызова трассировки
  • Это просто информативное свойство, а не управляющее свойство.
    • http не будет иметь другой логики выполнения из-за наличия или отсутствия трассировки
    • Это просто для информирования пользователей API, чтобы помочь пользователям записывать журналы или отлаживать
    • Таким образом, след здесь существует в контексте
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

Резюме Контекст

  • Для всех длинных блокирующих операций требуется контекст.
  • errgroup — хорошая абстракция, построенная поверх контекста.
  • Когда запрос заканчивается, отменить контекст
  • Context.Value следует использовать дляинформативный характервещи, а нехарактер контролявещи
  • Ограничение пространства ключей Context.Value
  • Context и Context.Value должны быть неизменными и должны быть потокобезопасными.
  • Контекст должен умереть вместе с Request dies

Q&A

Доступ к базе данных также использует контекст?

Как упоминалось ранее, Context используется для долгосрочных блокирующих операций, и то же самое верно для операций с базой данных. Однако для операции Cancel по тайм-ауту операция записи обычно не отменяется, а для операции чтения обычно имеется операция Cancel.

оригинальный

blog.horn99.org/post/go wave…

Личный публичный аккаунт WeChat:

Персональный гитхаб:

github.com/jiankunking

личный блог:

jiankunking.com