본문 바로가기
Language/Go

Go - context (blog)

by ocwokocw 2022. 11. 13.

- 출처: https://go.dev./blog/context

 

- 소개

Go 서버에서 들어오는 요청은 goroutine 으로 다루어지며, 요청 핸들러는 DB나 RPC 서비스 접근을 위해 추가적인 goroutine을 생성하기도 한다. 때로는 이런 goroutine 들이 공유해야 하는 값들(ex - end user 식별값, authorization token, 요청 deadline 등)이 있을 수 있다.
 
만약 요청이 취소되거나 timeout이 발생하면 사용중이던 자원을 회수해야 한다. go 에서는 이런 상황들을 해결할 수 있도록 Context를 제공한다.

- Context

context package의 핵심인 Context 형을 살펴보자.
 
// A Context carries a deadline, cancellation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}
 
Done 메소드는 실행중인 함수에 취소신호 역할을 하는 채널을 반환한다. Err는 왜 Context가 취소되었는지를 알려주는 메소드이다.
 
취소라는 말이 나오고 있는데 그렇다면 왜 Cancel과 같은 메소드는 없을까? Done 채널의 반환형을 자세히 살펴보면 수신 전용 채널인데 취소 신호를 받는 함수는 대게 취소 신호를 보내지 않기 때문이다. 예를 들어 부모가 sub-operation goroutine을 시작했다고 할 때, sub-operation 에서 부모를 취소하는 동작은 하지 않기 때문이다.
 
Value 메소드를 이용하면 요청 범위의 데이터를 Context에 실을 수 있다. 이때 데이터는 반드시 다중 goroutine 에서 동시에 수행되더라도 안전함을 보장해야 한다.

- 파생된 Context

context package 에는 기존의 Context로 부터 신규 Context를 파생할 수 있는 함수들이 있다. 파생된 Context는 tree 형태여서 어떤 context가 취소되면 해당 context로 부터 파생된 context 도 취소된다.
 
// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context
 
Background는 context의 root로서 절대 취소되지 않는다.
 
// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
 
WithCancel과 WithTimeout은 부모보다 빨리 취소될 수 있는 파생된 Context를 반환한다. 
 
// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context
 
WithValue는 요청범위의 값을 Context에 추가한다.

- Example: Google web search

golang 검색을 수행하면서 timeout에 설정된 값이 지나면 취소되는 /search?q=golang&timeout=1s 와 같은 요청을 처리하는 예제를 작성해보자.
 
Google web search는 크게 3개의 pakcage 로 구성된다.
 
  • server: main 함수, /search 를 다루는 handler
  • userip: User의 IP를 추출하여 context에 담는다.
  • google: query를 Google에 보내기 위한 Search 함수 구현
 
func handleSearch(w http.ResponseWriter, req *http.Request) {
  var (
    ctx    context.Context
    cancel context.CancelFunc
  )

  timeout, err := time.ParseDuration(req.FormValue("timeout"))
  if err == nil {
    ctx, cancel = context.WithTimeout(context.Background(), timeout)
  } else {
    ctx, cancel = context.WithCancel(context.Background())
  }
  defer cancel()

  query := req.FormValue("q")
  if query == "" {
    http.Error(w, "no query", http.StatusBadRequest)
  }

  userIp, err := userip.FromRequest(req)
  if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
  }

  ctx = userip.NewContext(ctx, userIp)

  start := time.Now()
  results, err := google.Search(ctx, query)
  elapsed := time.Since(start)
 
위의 코드는 server package 구현한 것이다.
 
요청에 timeout 인자 포함 여부에 따라 context를 다른 함수로 생성한다. WithTimeout 으로 context를 생성하면 설정한 시간이 지났을 때 취소된다. 요청으로 부터 IP를 추출하여 새로운 context를 생성하고 이를 google package의 검색에서 사용한다.
 
type key int

const userIPKey key = 0

func FromRequest(req *http.Request) (net.IP, error) {
  host, _, err := net.SplitHostPort(req.RemoteAddr)
  if err != nil {
    return nil, err
  }
  return net.ParseIP(host), nil
}

func NewContext(ctx context.Context, ip net.IP) context.Context {
  return context.WithValue(ctx, userIPKey, ip)
}

func FromContext(ctx context.Context) (net.IP, bool) {
  ip, ok := ctx.Value(userIPKey).(net.IP)
  return ip, ok
}
 
userip package 는 요청으로 부터 user 의 IP를 추출하여 context에 추가하는 함수를 정의하고 있다. context의 key 충돌을 피하기 위해서 unexported 된 별도 형을 정의해서 이를 key 로 사용하고 있다.
 
func httpDo(ctx context.Context, req *http.Request, f func(resp *http.Response, err error) error) error {
  c := make(chan error, 1)
  req = req.WithContext(ctx)
  go func() {
    c <- f(http.DefaultClient.Do(req))
  }()

  select {
  case <-ctx.Done():
    <-c
    return ctx.Err()
  case err := <-c:
    return err
  }
}
 
http 요청을 하는 httpDo는 context와 요청 그리고 요청 결과를 처리하는 함수 f를 인자로 받는다. context가 만료되면 곧바로 종료된다.
 
ctx.Done() 에서 곧바로 에러를 반환하지 않는 이유는 http 요청 결과를 응답할때까지 기다려주기 위함이다.
 
 

'Language > Go' 카테고리의 다른 글

Go - pipelines and cancellation  (0) 2022.10.21
package name base, util, or common  (0) 2022.06.26
Empty struct  (0) 2022.06.21
Zero value  (0) 2022.06.17
Error handling을 간단하게  (0) 2022.06.16

댓글