Плюсы и минусы Golang Context и предложения по использованию

Go

contextДизайн — довольно спорная тема в Голанге.contextЭто не серебряная пуля: хотя он решает некоторые проблемы, у него также есть много недостатков, которые люди критикуют. В этой статье в основном обсуждаютсяcontextпреимущества и недостатки и некоторые предложения по использованию.

недостаток

Потому что субъективно мне это тоже не очень нравится.contextдизайн, так что начнем с недостатков.

Повсюдуcontext

в соответствии сcontextОфициальная рекомендация по использованию,contextДолжен появиться в первом параметре функции. Это ведет прямо к коду вездеcontext. как вызывающая функция, даже если вы не собираетесь использоватьcontextвам также нужно передать заполнитель -context.Background()илиcontext.TODO(). Это определенно запах кода, особенно для программистов с чистотой кода, передача такого количества бессмысленных параметров просто недопустима.

Err()это очень грубо

context.Contextопределяется в интерфейсеErr()метод:

type Context interface {
    ...
	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error explaining why:
	// Canceled if the context was canceled
	// or DeadlineExceeded if the context's deadline passed.
	// After Err returns a non-nil error, successive calls to Err return the same error.
    Err() error
    ...
}

При срабатывании отмены (обычно это означает какие-то ошибки или аномалии),Err()метод, чтобы увидеть причину ошибки. Это действительно общее требование, ноcontextвнутри сумкиErr()Реализация кажется немного безвкусной,Err()Сообщаемые сообщения об ошибках ограничиваются следующими двумя:

  1. Отмена из-за отмены (извините???)
  2. Отменено из-за тайм-аута
// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")

// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

отErr()метод, вы вряд ли получите какие-либо сообщения об ошибках, связанных с бизнесом, то есть, если вы хотите узнать конкретную причину отмены, вы не можете ожидатьcontextСумка, ты должна сделать это сама. еслиcancel()Может быть лучше, чтобы метод получил ошибку:

	ctx := context.Background()
	c, cancel := context.WithCancel(ctx)
	err := errors.New("some error")
	cancel(err) //cancel的时候能带上错误原因

context.Value- Свобода без ограничений опасна

context.Value почтиmap[interface{}]interface{}:

type Context interface {
    ...
	Value(key interface{}) interface{}
    ...
}

Это дает программистам большой свободы, что почти хочет поставить то, что поставить. Но эта почти беспрепятственная свобода очень опасна, не только легко приведет к злоупотреблению, злоупотреблению и терять проверку типа компиляции, требует от насcontext.ValueКаждое значение в середине должно быть присвоено, чтобы предотвратитьpanic. Хотя в документации указаноcontext.ValueЕго следует использовать для сохранения данных типа «в области запроса», но для того, что «в области запроса», существует тысяча определений в глазах тысячи людей. рисунокrequest-id,access_token,user_idЭти данные могут быть размещены как «в области запроса».context.ValueЭто также может быть определено в структуре более ясным образом.

плохая читаемость

Плохая читабельность также является ценой свободы.При обучении чтению кода Go см.contextЭто головная боль. Если документация не четко прокомментирована, вы вряд ли узнаетеcontext.ValueЧто именно в нем содержится, не говоря уже о том, как правильно его использовать. Код нижеhttp.Requestв структуреcontextОпределение и примечания:

// http.Request 
type Request struct {
    ....
    // ctx is either the client or server context. It should only
	// be modified via copying the whole Request using WithContext.
	// It is unexported to prevent people from using Context wrong
	// and mutating the contexts held by callers of the same request.
	ctx context.Context
}

Ты видишь этоcontext.ValueБудет ли что-нибудь храниться внутри?

...
func main () {
    http.Handle("/", http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
	    fmt.Println(req.Context()) // 猜猜看这个context里面有什么?
    }))
}

Я пишу тебе, я не могу не подумать о том, что «конфеты» пытал душу: эти несколько бокалов вина на столе, который является стаканом маотаи?

даже если ты будешьcontextВы не можете сказать, распечатываете ли вы этоcontextСвязь с входными параметрами функции, возможно, в следующий раз будет передан другой набор параметров,contextЗначение внутри изменилось. Обычно это происходит, если документация непонятна (к сожалению, я обнаружил, что большая часть кода неcontext.Valueс четкими комментариями), можно искать только глобальноcontext.WithValue, линия по линии, чтобы найти.

преимущество

Хотя я субъективенcontextОпределенная «предвзятость» есть, но объективно в ней все же есть плюсы и плюсы.

Унифицировать метод реализации отмены

Во многих статьях говоритсяcontextрешеноПроблема отмены горутины, но на самом деле я думаю реализация самой отмены не проблема, используя закрывающийchannelФункции вещания, позволяющие отменить отмену, относительно просты, дайте каштаны:

// Cancel触发一个取消
func Cancel(c chan struct{}) {
	select {
	case <-c: //已经取消过了, 防止重复close
	default:
		close(c)
	}
}
// DoSomething做一些耗时操作,可以被cancel取消。
func DoSomething(cancel chan struct{}, arg Arg)  {
	rs := make(chan Result)
	go func() {
		// do something
		rs <- xxx  //返回处理结果
	}()
	select { 
	case <-cancel:
		log.Println("取消了")
	case result := <-rs:
		log.Println("处理完成")
	}
}

Или вы также можете использоватьchannelПоместите его в структуру:

type Task struct{
	Arg Arg
	cancel chan struct{} //取消channel
}
// NewTask 根据参数新建一个Task
func NewTask(arg Arg) *Task{
	return &Task{
		Arg:arg ,
		cancel:make(chan struct{}),
	}
}
// Cancel触发一个取消
func (t *Task) Cancel() {
	select {
	case <-t.c: //已经取消过了, 防止重复close
	default:
		close(t.c)
	}
}
// DoSomething做一些耗时操作,可以被cancel取消。
func (t *Task) DoSomething() {
	rs := make(chan Result)
	go func() {
		// do something
		rs <- xxx
	}()
	select {
	case <-t.cancel:
		log.Println("取消了")
	case result := <-rs:
		log.Println("处理完成")
	}
}
// t := NewTask(arg)
// t.DoSomething()

Видно, что реализация отмены также разнообразна. Тысяча программистов может написать тысячу реализаций. Но, к счастью, естьcontextРеализация отмены унифицирована, иначе при каждом обращении к библиотеке приходится изучать ее механизм отмены. я думаю этоcontextСамым большим преимуществом также является самый большой кредит. Сусликам нужно только видеть, что есть функция вcontext, Вы знаете, как отменить выполнение функции. Если вы хотите добиться отмены, приоритет отдаетсяcontext.

Предоставляет менее элегантный, но эффективный способ передачи по значению

context.ValueЭто обоюдоострый меч, и его недостатки упомянуты выше, но при правильном использовании недостатки также могут стать преимуществами.map[interface{}]interface{}Атрибут определяет, что он может хранить почти все.Если метод нужно отменить, он также должен иметь возможность получать любые данные, переданные вызывающей стороной, тогдаcontext.ValueЕще очень действенный способ. Как «использовать его правильно», пожалуйста, обратитесь к совету по использованию ниже.

contextРекомендации

Рассматривать только в том случае, если требуется отменаcontext

contextСуществуют две основные функции: отмена иcontext.Value. Если вам просто нужно передавать значения между горутинами, не используйтеcontext. Потому что в мире ГоcontextКак правило, его можно отменить по умолчанию, и нельзя отменить.contextЗвонящий легко может быть неправильно понят.

НеопределенныйcontextЭто не душа.

context.Valueможешь или нет

context.ValueДоступ к контенту должен быть обязанностью пользователя библиотеки.. Если это поток данных внутри самой библиотеки, пожалуйста, не используйтеcontext.Value, поскольку эта часть данных обычно является фиксированной и контролируемой. Предположим, что модулю аутентификации в системе нужна строкаtokenДля аутентификации, сравнивая следующие две реализации, очевидно, что дисплей будетtokenПроще передать как параметр.

// 用context
func IsAdminUser(ctx context.Context) bool {
  x := token.GetToken(ctx)
  userObject := auth.AuthenticateToken(x)
  return userObject.IsAdmin() || userObject.IsRoot()
}

// 不用context
func IsAdminUser(token string, authService AuthService) int {
  userObject := authService.AuthenticateToken(token)
  return userObject.IsAdmin() || userObject.IsRoot()
}

Пример исходного кода:How to correctly use context.Context in Go 1.7

Итак, забудьте о «области запроса», поставьтеcontext.ValueДумайте об этом как о "пользовательской области" - пусть пользователь, вызывающий библиотекуcontext.Valueчто туда положить.

использоватьNewContextиFromContextДоступcontext

Не используйте напрямуюcontext.WithValue()иcontext.Value("key")для доступа к данным,context.ValueУровень инкапсуляции может эффективно уменьшить избыточность кода, улучшить его читаемость и максимально предотвратить некоторые ошибки по невнимательности.context.ContextАннотации в интерфейсе дают нам хороший пример:

package user

import "context"

// User is the type of value stored in the Contexts.
type User struct {...}

// key is an unexported type for keys defined in this package.
// This prevents collisions with keys defined in other packages.
type key int

// userKey is the key for user.User values in Contexts. It is
// unexported; clients use user.NewContext and user.FromContext
// instead of using this key directly.
var userKey key

// NewContext returns a new Context that carries value u.
func NewContext(ctx context.Context, u *User) context.Context {
	return context.WithValue(ctx, userKey, u)
}

// FromContext returns the User value stored in ctx, if any.
func FromContext(ctx context.Context) (*User, bool) {
	u, ok := ctx.Value(userKey).(*User)
	return u, ok
}

При использованииcontext.Value, пожалуйста, прокомментируйте ясно

упомянутый выше,context.ValueЧитабельность очень плохая, поэтому приходится компенсировать это документацией и комментариями. хоть перечисли все что можноcontext.Valueи их методы установки (NewContext(),FromContext()), перечислите как можно больше функцийcontext.ValueОтношения между вами и людьми, которые читают или поддерживают ваш код, стали более заботливыми.

пакет для уменьшенияcontext.TODO()илиcontext.Background()

для тех, кто предоставляетcontextметод, но как вызывающая сторона мы его не используем, нам все равно нужно передатьcontext.TODO()илиcontext.Background(). Если ты не выносишь много бесполезногоcontextРаспространение кода, слой инкапсуляции можно выполнить следующими методами:

// 假设有如下查询方法,但我们几乎不使用其提供的context
func QueryContext(ctx context.Context, query string, args []NamedValue) (Rows, error) {
    ...
}
// 封装一下
func Query(query string, args []NamedValue) (Rows, error) {
    return QueryContext(context.Background(), query, args)
}

Другая ссылка