golang Context

chanbro·2023년 10월 5일

golang

목록 보기
1/1
post-thumbnail

Context란?

Golang에서 Context는 , 작업 가능한 시간, 작업 취소 등 작업의 흐름을 제어하는데 사용된다.
Context는 다음과 같이 생성하여 사용할 수 있다.
Context가 cancel 혹은 timeout으로 종료되면 context의 Done이 호출된다.

import "context"
// Cancel 
ctx, cancle := context.WithCancel(context.Backgroud())
// Deadline
ctx, cancel := context.WithDeadline(context.Background(), TIME)
// Timeout
ctx, cancel := context.WithTimeout(context.Background(), DURATION)

context.WithCancel

cancel을 사용하여 context를 종료 시킬때 사용
ctx, cancel := context.WithCancel(context.Background())

package main

import (
  "context"
  "fmt"
  "sync"
  "time"
)

var wg sync.WaitGroup

func main() {
  wg.Add(1)
  ctx, cancel := context.WithCancel(context.Background())

  go PrintTick(ctx)

  time.Sleep(5 * time.Second)
  cancel()

  wg.Wait()
}

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)
  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

Tick 함수로 생성한 채널에서 데이터가 들어오면, tick이라는 문자열을 화면에 출력하였고,
Context의 Done 함수 채널에서 데이터가 들어오면 화면에 Done 문자와 종료 이유(ctx.Err())를 표시하게 된다.
그후 WaitGroup의 Done 함수를 호출하였다.
이를 실행하면 다음과 같은 결과과 출력된다.

tick
tick
tick
tick
tick
Done: context canceled

tick이 5번 출력되고, 컨텍스트의 종료 이유가 화면에 표시 된 후, 프로그램이 종료되는 것을 확인할 수 있다.

context.WithDeadline

context의 Deadline은 작업 흐름(ex goroutine)을 언제까지 유지할지 결정할 때 사용한다.

ctx, cancel := context.WithDeadline(context.Background(), TIME)

package main

import (
  "context"
  "fmt"
  "sync"
  "time"
)

var wg sync.WaitGroup

func main() {
  wg.Add(1)

  d := time.Now().Add(3 * time.Second)
  ctx, cancel := context.WithDeadline(context.Background(), d)

  go PrintTick(ctx)

  time.Sleep(time.Second * 5)
  cancel()

  wg.Wait()
}

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)
  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

지금으로부터 3초후 Context를 종료시키기 위해 Deadline을 지정했다.
이를 실행하면 다음과 같은 결과를 얻을 수 있다.

tick
tick
tick
Done: context deadline exceeded

tick이 세번 화면에 표시된 후, Deadline에 의해 Context가 종료되는 것을 확인할 수 있다.
하지만, 메인 고루틴은 5초간 유지되므로, 5초후에 프로그램이 종료되는 것을 확인할 수 있다.

Deadline을 지정하여 Context를 종료시키더라도,
cancel 함수를 호출하여 Context를 닫아주어야 안전하다 .

context.WithTimeout

Context의 Timeout은 작업 흐름을 얼마간 유지할지 결정할 때 사용한다.

ctx, cancel := context.WithTimeout(context.Background(), TIME)

package main

import (
  "context"
  "fmt"
  "sync"
  "time"
)

var wg sync.WaitGroup

func main() {
  wg.Add(1)

  ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)

  go PrintTick(ctx)

  time.Sleep(time.Second * 5)
  cancel()

  wg.Wait()
}

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)
  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

이를 실행하면 다음과 같은 결과를 얻을 수 있다.

tick
tick
tick
Done: context deadline exceeded

결과는 Deadline을 사용하였을 때와 동일한 것을 알 수 있다.

둘의 차이점은 Deadline은 언제까지 유지할 것인지를 지정한다면, Timeout은 얼마간 유지할지를 지정한다.
Timeout 역시, Timeout에 의해 context를 종료시키더라도, cancel 함수를 호출하여 context를 닫아주어야 안전하다.

context.WithValue

Context의 WithValue를 사용하여 채널과 같이 서브 고루틴에 데이터를 전달할 수 있다.

ctx := context.WithValue(context.Background(), KEY, VALUE)
v := ctx.Value(KEY)

package main

import (
  "context"
  "fmt"
  "sync"
)

var wg sync.WaitGroup

func main() {
  wg.Add(1)

  ctx := context.WithValue(context.Background(), "v", 3)

  go square(ctx)

  wg.Wait()
}

func square(ctx context.Context) {
  if v := ctx.Value("v"); v != nil {
    n := v.(int)
    fmt.Println("Square:", n*n)
  }
  wg.Done()
}

이를 실행하면 다음과 같은 결과를 얻을 수 있다.

Square: 9

Context Wrapping

Golang에서 context는 다음과 같이 랩핑(Wrapping)하여 사용할 수 있다.

ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithValue(ctx, "key", "value")
ctx = context.WithValue(ctx, "key2", "value2")

package main

import (
  "context"
  "fmt"
  "sync"
  "time"
)

var wg sync.WaitGroup

func main() {
  wg.Add(1)
  ctx, cancel := context.WithCancel(context.Background())
  ctx = context.WithValue(ctx, "s", 2)

  go PrintTick(ctx)

  time.Sleep(5 * time.Second)
  cancel()

  wg.Wait()
}

func PrintTick(ctx context.Context) {
  tick := time.Tick(time.Second)

  if v := ctx.Value("s"); v != nil {
    s := v.(int)
    tick = time.Tick(time.Duration(s) * time.Second)
  }

  for {
    select {
    case <-ctx.Done():
      fmt.Println("Done:", ctx.Err())
      wg.Done()
      return
    case <-tick:
      fmt.Println("tick")
    }
  }
}

WithCancel 예제에서 WithValue를 사용하여 컨텍스트를 랩핑하였다.
context를 랩핑하여 컨텍스트를 통해 값을 전달하였으며,
전달받은 값을 사용하여 tick 문자열을 1초가 아닌 2초에 한번 표시되도록 하였다.
이를 실행하면 다음과 같은 결과를 얻을 수 있다.

tick
tick
Done: context canceled

마무리

WithCancel, WithDeadline, WithTimeout을 사용하여 컨텍스트를 정의하는 방법뿐만 아니라 WithValue를 사용하여 컨텍스트를 통해 데이터를 전달하는 방법, 컨텍스트를 랩핑하여 사용하는 방법에 대해서 정리하였다.

profile
이유있는 코드를 작성하는 서버 개발자

0개의 댓글