Go에서 컨텍스트는 조건과 같은 기능을 한다.
예를 들어 특정 go루틴에게
1. 내가 이 명령어를 사용하면 멈춰!
2. 이 시간까지만 작동해!
3. 이 시간동안만 작동해!
4. 이거 갖고 가!
등의 조건을 달아서 실행할 수 있다. 하나씩 예시와 함께 살펴보자
1. 멈춰!!! WithCancle()
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context) {
fmt.Println("background long running task launched")
select {
case <-ctx.Done():
fmt.Println("long running task bailed because context cancelled")
}
}
func main() {
// this will bail when cancelFunc is called
ctx, cancelFunc := context.WithCancel(context.Background())
go longRunningTask(ctx)
time.Sleep(1)
fmt.Println("background long running task still going")
time.Sleep(1)
fmt.Println("going to cancel background task")
cancelFunc()
time.Sleep(1)
fmt.Println("some time has elapsed after cancelling")
}
작동 과정은 다음과 같다.
longRunningTask 시작 => cancelFunc() 실행 => ctx.Done채널에 값 넣어짐 => longRunningTask에서 select로 ctx.Done채널의 값을 인지하고 종료됨
외부에서 go루틴을 종료할 수 있는 좋은 방법이다. 코드를 보면 context.Background()가 뭔지 궁금하다.
context.Background()?
ctx, cancelFunc := WithCancel(parent Context)
WithCancel은 인자로 parent Context를 받는다. 그래서 굳이 Background가 아닌 다른 context를 넣어줘도 무방하다. 하지만 다른 context도 parent가 필요할 것이다. 이런 반복 구조에서 결국 가장 기본이 되는 parent가 필요하고 이 parent를 Background가 담당한다.
Background로 생성한 context는 어떠한 조건도 가지지 않는다. 취소도, 기간도, 값도 가지지 않는 새하얀 도화지와 같다. 이 새하얀 도화지에 WithCancel(도화지)로 cancel할 수 있는 그림을 그린다. 이를 wrapping이라 한다.
근데 만약 WithCancel(WithCancel(도화지)) 와 같이(실제로 되는 코드는 아님) 도화지 위에 같은 그림을 그리면 우선 순위는 누가 갖을까 의문이 생긴다. 결론부터 말하면 parent가 우선시 된다. 만약 child가 cancel되지 않았는데 parent가 cancel됐다면 child도 같이 cancel된다. 하지만 child는 parent에 영향을 주지 않는다.
이 점을 고려하고 이어서 보자!
2. 이 시간까지만 작동해!! Deadline()
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context, timeToRun time.Duration) {
fmt.Println("start time: ", time.Now())
select {
case <-time.After(timeToRun):
fmt.Println("completed before context deadline passed")
case <-ctx.Done():
fmt.Println("bailed because context deadline passed")
}
fmt.Println("end time: ", time.Now())
}
const duration = 5 * time.Second
func main() {
ctx := context.Background()
// this will bail because the function runs longer than the context's deadline allows
ctx1, _ := context.WithDeadline(ctx, time.Now().Add(duration))
longRunningTask(ctx1, 10*time.Second)
// this will complete because the function completes before the context's deadline arrives
ctx2, _ := context.WithDeadline(ctx, time.Now().Add(duration))
longRunningTask(ctx2, 1*time.Second)
}
인자 : ctx, cancelFunc := WithDeadline(parent Context, deadline time.Time)
말 그대로 Deadline을 줘버리는 것이다. 위 코드를 실행해보면 ctx1과 ctx2의 결과가 다름을 알 수 있다.
ctx1에서의 경우는 longRunningTask가 Deadline으로 정한 시간보다 수행시간이 길어 중간에 탈출했다. 이를 bail이라 하는데 보석금을 내고 빨리 나왔다~ 라고 이해하면 편하다.
ctx2에서의 경우는 longRunningTask가 Deadline보다 빨리 끝나 기존 기능을 complete 했다.
여기서도 마찬가지로 만약 parent의 Deadline이 child의 Deadline보다 앞선다면 parent의 Deadline이 기준이 된다.
3. 이 시간동안만 작동해!! Timeout()
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context, timeToRun time.Duration) {
select {
case <-time.After(timeToRun):
fmt.Println("completed before context timed out")
case <-ctx.Done():
fmt.Println("bailed because context timed out")
}
}
const timeout = 5 * time.Second
func main() {
ctx := context.Background()
// this will bail because the function takes longer than the context allows
ctx1, _ := context.WithTimeout(ctx, timeout)
longRunningTask(ctx1, 10*time.Second)
// this will complete because the function completes before the context times out
ctx2, _ := context.WithTimeout(ctx, timeout)
longRunningTask(ctx2, 1*time.Second)
}
ctx, cancelFunc := WithTimeout(parent Context, dur time.Duration)
사용법은 위와 같다. 사실상 동작하는게 Deadline()과 별반 다르지 않아보인다...
실제로 WithTimeout()은 함수 내부에서 WithDeadline()을 호출한다. 즉, 기능은 동일하다는 것이다. 이 두 기능의 차이는 논리적 의미에서 있다.
"특정 시간이 되면 끝내라" vs "특정 시간 동안만 동작해라"
의 차이인듯 하다. (시간을 재는 시점이 다른것도 같은데 잘 모르겠다...)
상황에 맞게 잘 사용하면 될듯 하다!
4. 이거 갖고 가! Value
package main
import (
"context"
"fmt"
)
func tryAnotherKeyType(ctx context.Context, keyToConvert string) {
type keyType2 string
k := keyType2(keyToConvert)
if v := ctx.Value(k); v != nil {
fmt.Println("found a value for key type 2:", v)
} else {
fmt.Println("no value for key type 2")
}
}
func main() {
keyString := "foo"
type keyType1 string
k := keyType1(keyString)
ctx := context.WithValue(context.Background(), k, "bar")
if v := ctx.Value(k); v != nil {
fmt.Println("found a value for key type 1:", v)
} else {
fmt.Println("no value for key type 1")
}
tryAnotherKeyType(ctx, keyString)
}
ctx := WithValue(parent Context, key, val any)
특정 값을 (key, value) 형식으로 갖는 컨텍스트를 반환해준다. 코드를 보면 굳이 string을 새로운 custom 타입으로 정의해서 사용한다. WithValue의 key로 사용될 변수의 타입은 standard type(int, string)이면 안되기 때문이다. 이는 다른 패키지에서 사용하는 key와 타입과 value가 같으면 충돌이 생길수 있어 이와 같이 한다고 한다.
위의 코드를 실행해보면 tryAnotherKeyType()에서 사용한 key를 다시 새롭게 custom 타입으로 정의하면 key는 foo로 같지만 type이 KeyType2로 변해 해당 키는 존재하지 않음을 알린다.
즉, key를 생성할때 custom type & key name 을 동시에 확인한다.
이렇게 Go에서의 context에 대해 알아봤다. 참고한 사이트는 다음과 같다.
https://betterprogramming.pub/how-and-when-to-use-context-in-go-b365ddf42ae2
How and When to Use Context in Go
With a focus on concurrency
betterprogramming.pub
이외에도 있지만 많아서 패스
'IT 일기 > GO' 카테고리의 다른 글
Go 채널이란? (0) | 2023.04.02 |
---|---|
What is GoRoutine? (0) | 2023.03.31 |
Go Slice 사용법, 구조 그리고 append() 원리까지 Deep Dive (0) | 2023.03.29 |
Understand SOLID in Go (0) | 2023.03.24 |
vscode Terminal 설정 변경 (Go에서 sqlite3 사용하기) (0) | 2023.03.23 |