context
context 는 컨텍스트로 번역되며, 설계初衷은 여러 코루틴, 특히 부모 - 자식 코루틴 간에 신호와 간단한 데이터를 전달하는 데 있습니다. 이는 일반적으로 HTTP 요청 처리, 작업 스케줄링, 데이터베이스 쿼리 등의 시나리오에서 사용되며, 특히 마이크로서비스 아키텍처에서 gRPC 는 context 를 통해 프로세스 간 및 네트워크를 넘어 메타데이터 전달, 링크 제어 등의 작업을 수행합니다.
package main
import (
"context"
"fmt"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
defer stop()
for {
select {
case <-ctx.Done():
fmt.Println("terminate")
return
default:
}
fmt.Println("running")
time.Sleep(100 * time.Millisecond)
}
}위 사례에서 context 를 통해 프로세스 신호를 전달하며, 신호를 수신하면 프로그램이 자체 종료됩니다. 이 역시 context 의 응용 시나리오 중 하나입니다.
구조
context.Context 는 구체적인 구현이 아닌 일련의 메서드를 정의한 인터페이스입니다.
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline(), 마감 날짜와 종료 여부를 나타내는 불리언 값을 반환합니다.Done(), 종료 알림을 위한 채널을 반환합니다.Err(), 컨텍스트가 종료된 이유를 반환합니다.Value(), 키에 따라 지정된 값을 반환합니다.
표준 라이브러리에는 다음 몇 가지 사용 가능한 컨텍스트가 있습니다.
Background, 빈 컨텍스트로, 일반적으로 컨텍스트의 루트 노드로 사용됩니다.WithCancel,WithCancelCause, 취소 가능한 컨텍스트.WithDeadline,WithDeadlineCause, 마감 시간이 있는 컨텍스트.WithTimeout,WithTimeoutCause, 초과 시간이 있는 컨텍스트.WithValue, 값을 전달할 수 있는 컨텍스트.
구현에 있어서는 주로 다음이 있습니다.
timerCtxcancelCtxemptyCtx
따라서 핵심 기능은 네 가지입니다.
- 취소
- 마감
- 값 전달
- 전파
이 몇 가지를 이해하면 기본적으로 context 의 작동 원리를 알 수 있습니다.
취소
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
cause error // set to non-nil by the first cancel call
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}cancelCtx 의 핵심은 propagateCancel 메서드에 있으며, 이는 취소 가능한 행위를 부모 - 자식 컨텍스트에 전파하는 역할을 담당합니다.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}먼저 부모 context 가 취소 가능한지 확인하며, 불가능하면 바로 반환합니다.
godone := parent.Done() if done == nil { return // parent is never canceled }부모 context 가 이미 취소되었는지 확인하며, 취소되었다면 모든 자식 context 를 취소합니다.
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }부모 context 를
cancelCtx유형으로 변환하려고 시도하며, 성공하면 현재 context 를 부모 context 의children에 추가합니다.goif p, ok := parentCancelCtx(parent); ok { // parent is a *cancelCtx, or derives from one. p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err, p.cause) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() return }afterFuncer유형으로 변환하려고 시도하며, 성공하면 현재 context 취소 메서드를 부모 context 의AfterFunc에 등록합니다.goif a, ok := parent.(afterFuncer); ok { // parent implements an AfterFunc method. c.mu.Lock() stop := a.AfterFunc(func() { child.cancel(false, parent.Err(), Cause(parent)) }) c.Context = stopCtx{ Context: parent, stop: stop, } c.mu.Unlock() return }그래도 안 되면 별도의 코루틴을 열어
Done채널을 모니터링하며, 신호를 수신하면 자식 context 를 취소합니다.gogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
그런 다음 cancelCtx.cancel 메서드가 최종적으로 자식 context 취소를 담당합니다.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}플로우는 다음과 같습니다.
현재 context 가 이미 취소되었는지 확인합니다.
goif err == nil { panic("context: internal error: missing cancel error") } if cause == nil { cause = err } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled }done채널을 닫아 종료 알림을 보냅니다.goc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }자식 context 를 순회하며 알림을 보냅니다.
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()마지막으로 전달된 매개변수에 따라 부모 context 에서 제거할지 판단합니다.
if removeFromParent { removeChild(c.Context, c) }
마감
WithTimeout 과 WithDeadline 은 모두 마감 시간이 있는 컨텍스트로, 둘은 동일한 유형이며 사용 의미만 다를 뿐이며, 모두 cancelCtx 를 기반으로 합니다.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause 는 마감 시간이 있는 컨텍스트를 생성합니다.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}플로우는 다음과 같습니다.
마감 날짜를 확인하며, 부모 마감 날짜가 현재 마감 날짜보다 빠르면 부모 context 가 현재 context 보다 먼저 취소되므로
cancelCtx유형의 컨텍스트를 직접 생성합니다.goif parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) }timeCtx를 구축하고 자식 context 에 전파합니다.goc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)현재 마감 시간을 계산하며, 이미 마감되었으면 바로 취소합니다.
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }마감되지 않았다면
time.AfterFunc를 사용하여 마감 시간에 현재 context 를 취소하도록 설정합니다.goc.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded, cause) }) } return c, func() { c.cancel(true, Canceled, nil) }
timerCtx 의 경우 컨텍스트 취소 메서드는 단순히 timer 를 중지하고 자식 context 도 함께 중지합니다.
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}값 전달
valueCtx 는 컨텍스트에서 값을 전달하고 가져올 수 있습니다.
type valueCtx struct {
Context
key, val any
}
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}핵심은 value 함수에 있으며, 다른 context 구체적 구현도 이 함수를 통해 값을 가져옵니다. 예를 들어
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}value 함수는 큰 for 루프로, 지속적으로 상향 재귀하며 지정된 키의 값을 찾습니다.
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}다른 Context 유형의 처리:
*valueCtx:key가 현재 Context 의key와 일치하면key에 해당하는val을 반환합니다.*cancelCtx:key가cancelCtxKey와 일치하면 현재cancelCtx를 반환하며, 이는 자신 자신을 반환함을 의미합니다.withoutCancelCtx: 취소 기능이 없는 Context 를 나타내며,key가cancelCtxKey와 일치하면nil을 반환합니다.*timerCtx:key가cancelCtxKey와 일치하면 연관된cancelCtx를 반환합니다.backgroundCtx및todoCtx: 일반적으로 추가 값을携带하지 않는 특수 유형의 Context 로, 이 두 유형을 만나면 바로nil을 반환합니다.- 알 수 없는 유형인 경우
Value메서드를 계속 호출하여 찾습니다.
요약
이러한 context 의 핵심은 cancelCtx 로, 이를 통해 취소 신호를 전파하며, 다른 context 유형 및 일부 서드파티 context 유형도 이 위에 층층이 쌓아 다양한 기능을 구현합니다.
