Контекстный пакет языка Go короткий и компактный, что очень подходит для изучения новичками. Будь то исходный код или фактическое использование, стоит потратить время на изучение.
В этой статье все-таки хочется попытаться изучить его всесторонне и глубоко. По сравнению с предыдущими выпусками в целом статья не длинная, надеюсь вы что-то почерпнете после прочтения!
что такое контекст
Стандартная библиотека Go 1.7, чтобы представить контекст, китайский язык переводится как «контекст», это точный контекст goroutine, включая рабочий статус, среду и другую информацию goroutine на сайте.
Контекст в основном используется для передачи контекстной информации между горутинами, в том числе: сигнал отмены, тайм-аут, крайний срок, k-v и т. д.
Поэтому с введением пакета контекста многие интерфейсы в стандартной библиотеке добавили параметры контекста, такие как пакет базы данных/sql. context почти стал стандартной практикой для управления параллелизмом и тайм-аутом.
Значение типа context.Context может координировать код в нескольких подпрограммах для выполнения операций «отмены» и может хранить пары «ключ-значение». Самое главное, что это безопасно для параллелизма.
Все API, которые взаимодействуют с ним, могут выполнять операции «отмены» внешним управлением, например: отменять выполнение HTTP-запроса.
Не понимаю? Неважно, сначала оглянись.
почему контекст
Go часто используется для написания фоновых сервисов, обычно для создания http-сервера требуется всего несколько строк кода.
В Go-сервере обычно запускается одновременно несколько горутин на каждый запрос: некоторые обращаются к базе данных для получения данных, а некоторые вызывают нижестоящий интерфейс для получения связанных данных...
Эти горутины должны делиться основными данными запроса, такими как токен входа в систему, максимальное время ожидания для обработки запроса (если данные возвращаются после превышения этого значения, запросчик не может их получить из-за времени ожидания) и т. д. Когда запрос отменен или время обработки слишком велико, может случиться так, что пользователь закрыл браузер или превысил период ожидания, указанный запрашивающей стороной, и запрашивающая сторона напрямую отказывается от результата запроса. В этот момент все горутины, работающие над этим запросом, должны быстро выйти, потому что их «результаты работы» больше не нужны. После завершения всех связанных горутин система может вернуть связанные ресурсы.
Еще один момент, сервер на языке Go фактически представляет собой «модель сопрограммы», то есть сопрограмма обрабатывает запрос. Например, в пиковый период бизнеса ответ нижестоящей службы медленный, а текущий системный запрос не имеет контроля тайм-аута, или время тайм-аута установлено слишком большим, тогда будет все больше и больше сопрограмм, ожидающих выполнения. нижестоящий сервис для возврата данных. И мы знаем, что сопрограммы потребляют системные ресурсы, и следствием этого является то, что количество сопрограмм резко возрастает, использование памяти резко возрастает и даже делает службы недоступными. Более серьезные приведут к лавинному эффекту, весь сервис недоступен внешнему миру, это однозначно авария уровня P0. В это время кто-то должен взять на себя вину.
На самом деле описанной выше аварии на уровне P0 можно избежать, установив «разрешить максимальное время обработки в нисходящем направлении». Например, тайм-аут, установленный для нисходящего потока, составляет 50 мс.Если возвращаемые данные не будут получены после этого значения, клиенту будет возвращено значение по умолчанию или ошибка. Например, возвращает количество товара по умолчанию. Обратите внимание, что время ожидания, установленное здесь, отличается от времени ожидания чтения и записи, установленного при создании http-клиента, и не будет здесь подробно описываться. Проверьте ссылки【Go 在今日头条的实践】
Отличная статья.
Контекстный пакет был разработан для решения, упомянутых выше проблем: передача общих значений между группой Goroutines, отмена сигналов, сроков ...
Проще говоря, в Go мы не можем убить сопрограмму напрямую, закрытие сопрограммы обычно выполняется с помощьюchannel+select
способ управления. Но в некоторых сценариях, таких как обработка запроса, выводится много сопрограмм, и эти сопрограммы взаимосвязаны: какие-то глобальные переменные нужно расшарить, есть общий дедлайн и т. д., и их можно закрыть одновременно. повторное использованиеchannel+select
Это будет более хлопотно, чем это может быть достигнуто через контекст.
Одним словом: контекст используется для решения проблемы между горутинами退出通知
,元数据传递
функция.
Основной принцип реализации контекста
Версия Go, которую мы проанализировали, по-прежнему1.9.2
.
Общий обзор
Код контекстного пакета не длинный,context.go
Всего в файле меньше 500 строк, и в нем много больших комментариев, код может выглядеть только как 200 строк, это кодовая база, которую стоит изучить.
Давайте сначала посмотрим на общую картину:
тип | название | эффект |
---|---|---|
Context | интерфейс | Определены четыре метода интерфейса Context |
emptyCtx | структура | Реализует интерфейс Context, который на самом деле является пустым контекстом. |
CancelFunc | функция | функция отмены |
canceler | интерфейс | контекст отменяет интерфейс и определяет два метода |
cancelCtx | структура | можно отменить |
timerCtx | структура | Тайм-аут отменен |
valueCtx | структура | Может хранить пары k-v |
Background | функция | Возвращает пустой контекст, часто используемый в качестве корневого контекста. |
TODO | функция | Возвращает пустой контекст, часто используемый во время рефакторинга, когда подходящий контекст недоступен. |
WithCancel | функция | На основе родительского контекста создайте контекст, который можно отменить |
newCancelCtx | функция | Создать отменяемый контекст |
propagateCancel | функция | Передать отношение отмены между узлами контекста. |
parentCancelCtx | функция | Найдите первый отменяемый родительский узел |
removeChild | функция | Удалить дочерний узел родительского узла |
init | функция | инициализация пакета |
WithDeadline | функция | Создайте контекст с дедлайном |
WithTimeout | функция | Создать контекст с тайм-аутом |
WithValue | функция | Создайте контекст, в котором хранятся пары k-v |
В приведенной выше таблице показаны все функции, интерфейсы и структуры контекста. Вы можете получить общее представление о ситуации в целом. Вы можете оглянуться после прочтения статьи.
Общая диаграмма классов выглядит следующим образом:
интерфейс
Context
Теперь вы можете посмотреть непосредственно на исходный код:
type Context interface {
// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{}
// 在 channel Done 关闭后,返回 context 取消原因
Err() error
// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 获取 key 对应的 value
Value(key interface{}) interface{}
}
Context
это интерфейс, который определяет 4 метода, все из которых幂等
из. То есть вызов одного и того же метода несколько раз подряд даст один и тот же результат.
Done()
Возвращает канал, который может сигнализировать об отмене контекста: когда канал закрывается, контекст отменяется. Обратите внимание, что это канал только для чтения. Мы также знаем, что при чтении закрытого канала будет считано нулевое значение соответствующего типа. И некуда в исходниках запихивать значения в этот канал. Другими словами, этоreceive-only
канал. Таким образом, чтение этого канала в подпрограмме ничего не прочитает, если он не закрыт. Он также использует это преимущество: после того, как подпрограмма считывает значение (нулевое значение) из канала, она может сделать некоторую доработку и выйти как можно скорее.
Err()
Возвращает ошибку, указывающую, почему канал был закрыт. Например, было отменено или истекло время ожидания.
Deadline()
Возвращает крайний срок контекста.Через это время функция может решить, выполнять ли следующую операцию.Если времени слишком мало, то ее нельзя делать, иначе будет трата системных ресурсов. Конечно, этот крайний срок также можно использовать для установки тайм-аута для операции ввода-вывода.
Value()
Получить значение, соответствующее ранее заданному ключу.
canceler
Давайте посмотрим на другой интерфейс:
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
Контекст, который реализует два метода, определенных выше, указывает, что Контекст можно отменить. В исходном коде есть два типа, которые реализуют интерфейс отмены:*cancelCtx
и*timerCtx
. Обратите внимание, что добавлено*
Дело в том, что указатели этих двух структур реализуют интерфейс отмены.
Причина, по которой интерфейс Context спроектирован так:
- «Отмена» операция должна быть консультативной, не обязательной
Звонящий не должен заботиться, помехите случай Callee, чтобы решить, как и когда вернуться - ответственность Callee. Caller Просто отправьте сообщение «Отменить», Callee в соответствии с дополнительной информацией о решении, полученной, поэтому интерфейс не определяет метод отмены.
- Действие "Отмена" должно быть транзитивным
Когда вы «отменяете» функцию, другие связанные с ней функции также должны «отменяться». следовательно,Done()
Метод возвращает канал только для чтения, на котором прослушиваются все связанные функции. Как только канал закрыт, все слушатели могут получать его через «механизм вещания» канала.
структура
emptyCtx
определено в исходном кодеContext
После интерфейса и реализации дается:
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
Глядя на этот исходный код, очень счастлив. Поскольку реализация каждой функции очень проста, возвращайте либо напрямую, либо возвращайте nil.
Итак, на самом деле это пустой контекст, который никогда не будет отменен, без сохраненного значения и без крайнего срока.
Он упакован как:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
Он доступен для общественности через следующие две экспортируемые функции (с заглавной буквы):
func Background() Context {
return background
}
func TODO() Context {
return todo
}
background обычно используется в функции main как корневой узел всех контекстов.
todo обычно используется, когда вы не знаете, какой контекст передать. Например, при вызове функции, которой нужно передать параметр контекста, и у вас нет другого контекста для передачи, вы можете передать todo. Это часто происходит, когда идет рефакторинг, добавление параметра Context к некоторым функциям, но вы не знаете, что передать, поэтому используйте todo, чтобы «занять место» и в конечном итоге заменить его другим контекстом.
cancelCtx
Давайте посмотрим на важный контекст:
type cancelCtx struct {
Context
// 保护之后的字段
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
Это отменяемый контекст, который реализует интерфейс отмены. Он напрямую принимает контекст интерфейса как одно из своих анонимных полей, так что его можно рассматривать как контекст.
Первый взглядDone()
Реализация метода:
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
c.done создается "ленивым парнем" и будет создан только при вызове метода Done(). Опять же, функция возвращает канал только для чтения, а записать данные в этот канал некуда. Поэтому, если вы напрямую вызовете и прочитаете этот канал, сопрограмма будет заблокирована. Обычно используется с выбором. Как только он выключен, сразу считывается нулевое значение.
Err()
иString()
Способ относительно простой, особо и нечего сказать. Рекомендуется посмотреть исходный код, он очень простой.
Далее мы сосредоточимся наcancel()
Реализация метода:
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 必须要传 err
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经被其他协程取消
}
// 给 err 字段赋值
c.err = err
// 关闭 channel,通知其他协程
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// 遍历它的所有子节点
for child := range c.children {
// 递归地取消所有子节点
child.cancel(false, err)
}
// 将子节点置空
c.children = nil
c.mu.Unlock()
if removeFromParent {
// 从父节点中移除自己
removeChild(c.Context, c)
}
}
Общий,cancel()
Функция метода состоит в том, чтобы закрыть канал: c.done, рекурсивно отменить все его дочерние элементы, удалить себя из родительского узла. Достигаемый эффект заключается в том, что при закрытии канала сигнал отмены передается всем его дочерним элементам. Горутина получает сигнал отмены в операторе select.读 c.done
Выбрано.
Давайте еще раз посмотрим на метод создания отменяемого контекста:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
Это метод, доступный пользователю, передающий родительский контекст (обычно этоbackground
, как корневой узел), возвращает новый контекст, новый контекст делается, канал новый (о чем говорилось ранее).
Когда CancelFunc, возвращаемый функцией WithCancel, вызывается или канал done родительского узла закрывается (вызывается CancelFunc родительского узла), канал done контекста (дочерний узел) также будет закрыт.
Обратите внимание на параметр, переданный в метод WithCancel, первый истинен, то есть при отмене нужно удалить себя из родительского узла. Второй параметр — фиксированный тип ошибки отмены:
var Canceled = errors.New("context canceled")
Также обратите внимание, что при вызове метода отмены дочернего узла первый параметр, переданный вremoveFromParent
является ложным.
Необходимо ответить на два вопроса: 1. Когда будет передана истина? 2. Почему иногда правда, а иногда ложь?
когдаremoveFromParent
Когда TRUE, контекст текущего узла будет удален из контекста родителей:
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
Самая критическая строка:
delete(p.children, child)
Когда правда будет передана? Ответ - позвонитьWithCancel()
метод, то есть когда создается новый отменяемый узел контекста, возвращаемая функция cancelFunc будет проходить в true. Результат этого таков: при вызове возвращенного cancelFunc контекст будет "удален" из его родительского узла, потому что у родительского узла может быть много дочерних узлов, вы сами его отменяете, поэтому я хочу разорвать отношения с вами , да Другие не затрагиваются.
Внутри функции отмены я знаю, что все мои дети будут связаны с моим:c.children = nil
превратился в пепел. Естественно, для меня нет необходимости делать этот шаг. В конце концов, все мои детские узлы будут отрезаны от меня, поэтому нет необходимости делать их один за другим. Кроме того, если вы вызываете функцию Child.cancel и пропустите True при прохождении дочерних узлов, он также приведет к тому, что ситуация будет проходить и удалять карту одновременно, что будет проблематичным.
Как показано на левом рисунке выше, он представляет контекстное дерево. Когда вызывается метод отмены контекста, отмеченного красным на левом изображении, контекст удаляется из его родительского контекста: сплошная стрелка становится пунктирной линией. И контексты, обрамленные пунктирными кружками, были отменены, и родительско-дочерние отношения между контекстами в круге исчезли.
сконцентрируйсяpropagateCancel()
:
func propagateCancel(parent Context, child canceler) {
// 父节点是个空节点
if parent.Done() == nil {
return // parent is never canceled
}
// 找到可以取消的父 context
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// 父节点已经被取消了,本节点(子节点)也要取消
child.cancel(false, p.err)
} else {
// 父节点未取消
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// "挂到"父节点上
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 如果没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
Функция этого метода заключается в поиске «отменяемого» контекста, который может быть «присоединен», и «присоединении» к нему. Таким образом, когда вызывается метод отмены верхнего уровня, его можно передавать слой за слоем, чтобы одновременно «отменить» прикрепленные дочерние контексты.
Здесь мы сосредоточимся на объяснении того, почему возникает ситуация, описанная else.else
Это означает, что текущий контекст узла не находит родительский узел, который можно отменить, тогда необходимо запустить другую сопрограмму для отслеживания отмены родительского узла или дочернего узла.
Здесь возникает вопрос: так как родительский узел, который можно отменить, не найден, тоcase <-parent.Done()
Этот случай никогда не произойдет, поэтому этот случай можно игнорировать; иcase <-child.Done()
Этот случай ничего не делает. что этоelse
Разве это не лишнее?
вообще-то нет. ПосмотримparentCancelCtx
код:
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}
Здесь распознаются только три типа контекста: *cancelCtx, *timerCtx, *valueCtx. Если Context встроен в тип, он не будет распознан.
Поскольку кода пакета Context не так много, я скопирую его напрямую, а затем добавлю несколько печатных операторов в оператор else, чтобы проверить вышесказанное:
type MyContext struct {
// 这里的 Context 是我 copy 出来的,所以前面不用加 context.
Context
}
func main() {
childCancel := true
parentCtx, parentFunc := WithCancel(Background())
mctx := MyContext{parentCtx}
childCtx, childFun := WithCancel(mctx)
if childCancel {
childFun()
} else {
parentFunc()
}
fmt.Println(parentCtx)
fmt.Println(mctx)
fmt.Println(childCtx)
// 防止主协程退出太快,子协程来不及打印
time.Sleep(10 * time.Second)
}
Я не буду публиковать операторы печати, которые я добавил в else.Если вам интересно, вы можете поэкспериментировать сами. Давайте посмотрим на результаты печати трех контекстов:
context.Background.WithCancel
{context.Background.WithCancel}
{context.Background.WithCancel}.WithCancel
Разумеется, mctx, childCtx — это не то же самое, что обычный parentCtx, потому что это пользовательский тип структуры.
else
Этот код показывает, что если вы добавите ctx в структуру и используете его в качестве родительского узла, то при вызове функции WithCancel для создания контекста дочернего узла Go запустит новую сопрограмму для отслеживания сигнала отмены, который, очевидно, является напрасно тратить.
Опять же, два случая в операторе select не могут быть удалены.
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
В первом случае говорится, что когда родительский узел отменяется, дочерний узел отменяется. Если этот случай удален, отмененный сигнал родительского узла не может быть передан дочернему узлу.
Второй случай заключается в том, что если дочерний узел отменяет себя, то выход из выбора, а сигнал отмены родительского узла игнорируется. Если этот случай убрать, то очень вероятно, что родительская нода не была отменена, и горутина даст утечку. Конечно, если родительский узел отменяется, дочерний узел будет неоднократно отменяться, но это не имеет никакого эффекта.
timerCtx
timerCtx основан на cancelCtx, просто еще один time.Timer и один крайний срок. Таймер автоматически отменит контекст, когда наступит крайний срок.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx сначала является cancelCtx, поэтому его можно отменить. Взгляните на метод cancel():
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 直接调用 cancelCtx 的取消方法
c.cancelCtx.cancel(false, err)
if removeFromParent {
// 从父节点中删除子节点
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
// 关掉定时器,这样,在deadline 到来时,不会再次取消
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
Как создать timerCtx:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeout
функция вызывается напрямуюWithDeadline
, входящий крайний срок - это текущее время плюс время тайм-аута, то есть, если время тайм-аута проходит с этого момента, оно будет тайм-аут. Это,WithDeadline
Требуется абсолютное время. Сосредоточьтесь на нем:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// 如果父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。
// 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。
// 所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数
return WithCancel(parent)
}
// 构建 timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
// 挂靠到父节点上
propagateCancel(parent, c)
// 计算当前距离 deadline 的时间
d := time.Until(deadline)
if d <= 0 {
// 直接取消
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// d 时间后,timer 会自动调用 cancel 函数。自动取消
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
Это все еще поместите дочерние узлы, связанные с родительским узлом, родительский узел один раз отменял, отмененный сигнал будет передан до дочерних узлов, детские узлы будут отменены.
Существует особый случай, когда крайний срок создания дочернего узла позже, чем срок создания родительского узла, то есть если родительский узел автоматически отменяется, когда время истекло, то дочерний узел будет отменен. , в результате чего крайний срок дочернего узла вообще не работает. , потому что дочерний узел был отменен родительским узлом до истечения крайнего срока.
Основное предложение этой функции:
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
c.timer автоматически вызовет функцию отмены через интервал времени d, и входящая ошибкаDeadlineExceeded
:
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
То есть ошибка тайм-аута.
valueCtx
type valueCtx struct {
Context
key, val interface{}
}
Это реализует два метода:
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
Поскольку он напрямую использует Context как анонимное поле, хотя реализует только 2 метода, остальные методы наследуются от родительского контекста. Но это по-прежнему контекст, который является особенностью языка Go.
Функция для создания valueCtx:
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
Требованием к ключу является сравнимость, потому что значение в контексте необходимо получить через ключ позже, а сравнимость необходима.
Передайте контекст слой за слоем и, наконец, сформируйте такое дерево:
Это немного похоже на связанный список, но в противоположном направлении: контекст указывает на его родительский узел, а связанный список указывает на следующий узел. С помощью функции WithValue вы можете создавать слои valueCtx для хранения переменных, которые могут быть разделены между горутинами.
Процесс получения значения на самом деле представляет собой процесс рекурсивного поиска:
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
Он будет искать по ссылке и сравнивать ключ текущего узла Является ли это ключом, который нужно найти, если да, верните значение напрямую. В противном случае продолжайте двигаться вперед по контексту и, наконец, найдите корневой узел (обычно emptyCtx) и напрямую верните nil. Поэтому при использовании метода Value вам необходимо определить, является ли результат нулевым.
Поскольку направление поиска восходящее, родительский узел не может получить значение, сохраненное дочерним узлом, но дочерний узел может получить значение родительского узла.
WithValue
Процесс создания узла контекста на самом деле является процессом создания узла связанного списка. Значения ключей двух узлов могут быть равны, но это два разных узла контекста. При поиске он будет искать последний смонтированный узел контекста, который является контекстом родительского узла, который относительно близок. Итак, в целом сWithValue
На самом деле конструкция представляет собой неэффективный связанный список.
Если вы взялись за проект, вы наверняка сталкивались с такой дилеммой: в процессе обработки есть несколько подфункций и подпрограмм. Различные места будут вставлять в контекст различные пары k-v и в конечном итоге где-то их использовать.
Вы просто не знаете, когда и куда передать какое значение? Будут ли эти значения «затерты» (нижний слой — это два разных узла контекста, при поиске вверх возвращается только один результат)? Вы обязательно разобьетесь.
Это тожеcontext.Value
Самые спорные области. Многие люди советуют отказаться от традиционных ценностей по контексту.
Как использовать контекст
контекст очень удобен в использовании. Исходный код предоставляет функцию для создания контекста корневого узла:
func Background() Context
background является пустым контекстом, его нельзя отменить, он не имеет значения и не имеет времени ожидания.
В контексте корневого узла предоставляются четыре функции для создания контекстов дочерних узлов:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
Контекст передается между проходами функции. Просто вызовите функцию отмены в нужное время, чтобы подать горутинам сигнал об отмене, или вызовите функцию Value, чтобы получить значение в контексте.
В официальном блоге контекст для использования некоторые предложения:
- Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.
- Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
- Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
- The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
Позвольте мне перевести:
- Не помещайте Context в структуры. Тип Context напрямую используется в качестве первого параметра функции и обычно называется ctx.
- Не передавайте нулевой контекст функции.Если вы действительно не знаете, что передать, в стандартной библиотеке есть готовый контекст: todo.
- Не добавляйте в контекст тип, который следует использовать в качестве параметра функции, контекст должен хранить некоторые общие данные. Например: сеанс входа в систему, файл cookie и т. д.
- Один и тот же контекст может быть передан нескольким горутинам, не беспокойтесь, контекст безопасен для параллелизма.
передавать общие данные
Для разработки веб-сервера часто желательно обработать запрос, который сильно зависит от локального потока, который уникален для одного уравнения и не имеет этого понятия в языке Go, поэтому необходимо передать Контекст при вызове функции.
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
process(ctx)
ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
process(ctx)
}
func process(ctx context.Context) {
traceId, ok := ctx.Value("traceId").(string)
if ok {
fmt.Printf("process over. trace_id=%s\n", traceId)
} else {
fmt.Printf("process over. no trace_id\n")
}
}
результат операции:
process over. no trace_id
process over. trace_id=qcrao-2019
Когда функция процесса вызывается в первый раз, ctx является пустым контекстом, и, естественно, нельзя получить traceId. второй раз черезWithValue
Функция создает контекст и присваивает егоtraceId
Этот ключ естественно можно вынести из входящего значения value.
Конечно, в реальном сценарии это может быть Request-ID, полученный из HTTP-запроса. Таким образом, следующий пример может быть более подходящим:
const requestIDKey int = 0
func WithRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
// 从 header 中提取 request-id
reqID := req.Header.Get("X-Request-ID")
// 创建 valueCtx。使用自定义的类型,不容易冲突
ctx := context.WithValue(
req.Context(), requestIDKey, reqID)
// 创建新的请求
req = req.WithContext(ctx)
// 调用 HTTP 处理函数
next.ServeHTTP(rw, req)
}
)
}
// 获取 request-id
func GetRequestID(ctx context.Context) string {
ctx.Value(requestIDKey).(string)
}
func Handle(rw http.ResponseWriter, req *http.Request) {
// 拿到 reqId,后面可以记录日志等等
reqID := GetRequestID(req.Context())
...
}
func main() {
handler := WithRequestID(http.HandlerFunc(Handle))
http.ListenAndServe("/", handler)
}
отменить горутину
Давайте сначала представим сценарий: открываем страницу заказа на вынос, на карте отображается местоположение брата на вынос, и оно обновляется раз в секунду. После того, как сторона приложения инициирует запрос подключения к веб-сокету (возможно, в действительности опрашивая) к фону, фон запускает сопрограмму, вычисляет положение младшего брата каждую 1 секунду и отправляет его в конец. Если пользователь покидает эту страницу, фон должен «отменить» этот процесс, выйти из горутины, и система переработает ресурсы.
Возможная реализация бэкенда выглядит следующим образом:
func Perform() {
for {
calculatePos()
sendResult()
time.Sleep(time.Second)
}
}
Если вам нужно реализовать функцию «отмены», и вы не знаете контекстную функцию, вы можете сделать это: добавить в функцию логическую переменную указателя и в начале оператора for определить, что логическая переменная передается от истинного к ложному, если изменено, выход из цикла.
Простой метод, приведенный выше, может достичь желаемого эффекта, без проблем, но он не элегантен, и, поскольку количество сопрограмм велико и имеет разную вложенность, это будет очень хлопотно. Элегантный способ — использовать контекст естественным образом.
func Perform(ctx context.Context) {
for {
calculatePos()
sendResult()
select {
case <-ctx.Done():
// 被取消,直接返回
return
case <-time.After(time.Second):
// block 1 秒钟
}
}
}
Основной процесс может выглядеть так:
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
go Perform(ctx)
// ……
// app 端返回页面,调用cancel 函数
cancel()
Обратите внимание: контекст и функция cancelFun, возвращаемые функцией WithTimeOut, являются отдельными. Сам контекст не имеет функции отмены, причина этого в том, что функция отмены может быть вызвана только внешней функцией, не позволяя контексту дочернего узла вызывать функцию отмены, тем самым строго контролируя поток информации: от родителя контекст узла в контекст дочернего узла.
Предотвратить утечку Goroutine
В предыдущем примере горутина все равно будет выполняться и, наконец, будет возвращена, но это приведет к трате некоторых системных ресурсов. Здесь: «Если вы не используете Context для отмены, Goroutine выдаст пример из ссылки:【避免协程泄漏】
.
func gen() <-chan int {
ch := make(chan int)
go func() {
var n int
for {
ch <- n
n++
time.Sleep(time.Second)
}
}()
return ch
}
Это сопрограмма, которая может генерировать бесконечные целые числа, но если мне нужны только первые 5 чисел, которые она производит, возникает утечка горутины:
func main() {
for n := range gen() {
fmt.Println(n)
if n == 5 {
break
}
}
// ……
}
Когда n == 5, ломаться напрямую. Затем сопрограмма функции gen будет выполнять бесконечный цикл и никогда не останавливаться. Произошла утечка горутины.
Улучшите этот пример с помощью контекста:
func gen(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
var n int
for {
select {
case <-ctx.Done():
return
case ch <- n:
n++
time.Sleep(time.Second)
}
}
}()
return ch
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancel()
break
}
}
// ……
}
Добавьте контекст, вызовите функцию отмены перед разрывом и отмените горутину. После того, как функция gen получает сигнал отмены, она сразу же завершает работу, и система повторно использует ресурсы.
контекст на самом деле это хорошо
После прочтения полного текста у вас должно возникнуть такое ощущение: контекст предназначен для Сервера. Скажите, что для обработки запроса вам нужно запустить несколько Горутин параллельно для обработки, и между этими Горутинами также доставляются некоторые общие данные, которые пишутся путем написания Сервера.
Да, Go отлично подходит для написания серверов, но это язык общего назначения. Когда вы используете Go для решения вышеуказанных задач в Leetcode, вы точно не подумаете, что он чем-то отличается от обычного языка. Поэтому независимо от того, хороши многие функции или нет, мы должны начать сGo 只是一门普通的语言,很擅长写 server
с точки.
С этой точки зрения контекст не так хорош. Go официально рекомендует использовать Context в качестве первого параметра функции и даже подготовить имя. Это имеет следствие: поскольку мы хотим контролировать отмену всех сопрограмм, нам нужно добавить параметр Context почти ко всем функциям. Вскоре в нашем коде контекст будет распространяться повсюду, как вирус.
в ссылке【Go2 应该去掉 context】
В этом англоязычном блоге автор даже пошутил, что если вы хотите добавить параметры контекста в большинство функций стандартной библиотеки Go, например, так:
n, err := r.Read(context.TODO(), p)
Просто дай мне шанс!
В оригинальном тексте говорится:put a bullet in my head, please.
Увидев это предложение, я понимающе улыбнулась. Это может быть то, что сказал Тао Юаньмин: всякий раз, когда он что-то узнает, он с радостью забывает поесть. Конечно, я собираюсь посмотреть его за ужином.
Чтобы показать, что у него плохое представление о контексте, автор продолжил: «Если вы используете ctx.Value в моей (несуществующей) компании, вы уволены. Это так забавно, ха-ха.
Так же какWithCancel
,WithDeadline
,WithTimeout
,WithValue
Эти функции создания фактически создают узел связанного списка один за другим. Мы знаем, что операции над связанными списками обычноO(n)
Сложный и неэффективный.
Итак, какую проблему решает пакет контекста? ответ:cancelation
. Хотя он не идеален, он довольно аккуратно решает проблему.
Суммировать
К этому моменту содержимое всего пакета контекста закончено. Исходный код очень короткий, очень подходит для изучения, обязательно прочитайте.
Пакет context — это стандартная библиотека, представленная в Go 1.7, которая в основном используется для передачи сигналов отмены, тайм-аутов, крайних сроков и некоторых общих значений между горутинами. Это не идеально, но это почти стандартная практика для управления параллелизмом и тайм-аутом.
При использовании сначала создайте контекст корневого узла, а затем создайте контекст подузла соответствующей функции в соответствии с четырьмя функциями, предоставляемыми библиотекой. Поскольку он безопасен для параллелизма, его можно передавать с уверенностью.
При использовании контекста в качестве параметра функции поместите его непосредственно в позицию первого параметра и назовите его ctx. Кроме того, не вставляйте контекст в пользовательские типы.
Наконец, в следующий раз, когда вы увидите использование контекста в коде и посмотрите, как он используется, вы определенно не сможете избежать типов, о которых мы говорим. Ознакомившись с ним, вы обнаружите, что контекст может быть не идеальным, но он решает проблему лаконично и эффективно.
использованная литература
【контекст официальный блог】blog.golang.org/context
[Сегодняшняя практика Toutiao по построению Go]zhuanlan.zhihu.com/p/26695984
【Безжалостный блог Fixue】woohoo.fly snow.org/2017/05/12/…
[исходный код контекста]nuggets.capable/post/684490…
[Чтение исходного кода облака Tencent]cloud.Tencent.com/developer/ это…
[Мыслить более макроскопически, английский]Проклятые бедра.GitHub.IO/post/con TeX…
[Избегайте утечек сопрограмм]rakyll.org/leakingctx/
【Классификация приложений】Мечтатель Jonson.com/2019/05/09/…
[Официальная документация Пример перевода]В противном случае голова .Github.io/2017/05/19/...
[Пример, английский]Боюсь. AG вы горячая easy.com/post/under это…
[Go2 должен удалить контекст]Препятствование криминалистическому тестированию.GitHub.IO/post/con TeX…
[Исходный код, подробнее]nuggets.capable/post/684490…
[Является ли Golang Context хорошим дизайном? 】сегмент fault.com/ah/119000001…
[Сегодняшняя тренировка Toutiao в Го]36kr.com/p/5073181
【Пример】zhuanlan.zhihu.com/p/60180409