Goroutines
-
Goroutine Scheduling in Go
- Managed by the Go Runtime Scheduler
- Uses M:N Scheduling Model
- M goroutines are mapped onto N operating system threads
- Efficient Multiplexing (switching)
-
Concurrency vs Parellelism
- Concurrency : multiple tasks progress simultaneously and not necessarily at the same time
- Parellelism : tasks are executed literally at the same time on mutliple processors
-
Common Pitfalls and Best Practices
- Avoiding Goroutine Leaks
- Limiting Goroutine Creation
- Proper Error Handling
- Synchronization
Channels
Why Use Channels?
Channels are a way for go routines to communicate with each other and synchronize their execution
- 동시 실행되는 고루틴(Goroutines) 간의 안전하고 효율적인 통신을 가능하게 함
- 동시성 프로그램에서 데이터 흐름을 동기화하고 관리하는 데 도움
Channels Basics
- 채널 생성:
make(chan Type)
- 데이터 송수신:
<-
연산자 사용 - 채널 방향성:
- 송신 전용:
ch <- value
- 수신 전용:
value := <- ch
- 송신 전용:
// 채널 생성
ch := make(chan int)
// 고루틴에서 데이터 전송
go func() {
ch <- 42
}()
// 메인에서 데이터 수신
value := <-ch
fmt.Println(value) // 출력: 42
Unbuffered Channel
왜 아래 코드는 실행되지 않는가?
func main() {
greeting := make(chan string)
greetString := "Hello"
greeting <- greetString // 여기서 데드락 발생!
receiver := <-greeting
fmt.Println(receiver)
}
문제점: 데드락 발생
make(chan string)
으로 언버퍼드 채널을 생성greeting <- greetString
에서 채널에 데이터를 송신하려고 시도- 언버퍼드 채널은 동시에 송신자와 수신자가 있어야 데이터 전송이 가능
- 하지만 현재 수신자가 없음 (수신 코드는 다음 줄에 있음)
- 송신 작업이 무한정 블로킹되어 프로그램이 멈춤
왜 아래 코드는 실행되는가?
func fixed_main() {
greeting := make(chan string)
greetString := "Hello"
go func() {
greeting <- greetString // 별도 고루틴에서 송신
}()
receiver := <-greeting // 메인 고루틴에서 수신
fmt.Println(receiver)
}
해결책: 별도의 고루틴 사용
go func()
로 새로운 고루틴 생성- 송신자: 별도 고루틴에서
greeting <- greetString
실행 - 수신자: 메인 고루틴에서
<-greeting
실행 - 동시에 송신자와 수신자가 존재하므로 데이터 전송 성공
채널에서 수신이 블로킹되는 이유
언버퍼드 채널의 특성
- 동기적(Synchronous): 송신자와 수신자가 동시에 준비되어야 함
- 핸드셰이크 방식: 송신자가 데이터를 보내고 수신자가 받을 때까지 둘 다 대기
- 버퍼 없음: 데이터를 임시 저장할 공간이 없음
블로킹 시나리오
// 시나리오 1: 수신자가 없을 때 송신 블로킹
ch <- "data" // 수신자가 나타날 때까지 무한 대기
// 시나리오 2: 송신자가 없을 때 수신 블로킹
data := <-ch // 송신자가 데이터를 보낼 때까지 무한 대기
Buffered Channel
버퍼드 채널을 사용하는 이유
- 비동기 통신(Asynchronous Communication)
- 부하 분산(Load Balancing)
- 흐름 제어(Flow Control)
버퍼드 채널 생성
make(chan Type, capacity)
- 버퍼 용량(Buffer Capacity)
채널 버퍼링의 핵심 개념
- 블로킹 동작(Blocking Behavior)
- 논블로킹 작업(Non-Blocking Operations)
- 성능에 미치는 영향(Impact on Performance)
버퍼드 채널 사용 모범 사례
- 과도한 버퍼링 피하기(Avoid Over-Buffering)
- 우아한 종료(Graceful Shutdown)
- 버퍼 사용량 모니터링(Monitoring Buffer Usage)
버퍼드 채널 동작 원리
// 언버퍼드 채널 (동기적)
unbuffered := make(chan int)
unbuffered <- 1 // 수신자가 있을 때까지 블로킹
// 버퍼드 채널 (비동기적)
buffered := make(chan int, 3)
buffered <- 1 // 즉시 완료 (버퍼에 공간 있음)
buffered <- 2 // 즉시 완료
buffered <- 3 // 즉시 완료
buffered <- 4 // 이제 블로킹 (버퍼 가득참)
버퍼 상태에 따른 동작
- 버퍼에 공간이 있을 때: 송신 즉시 완료
- 버퍼가 가득 찰 때: 송신자 블로킹
- 버퍼에 데이터가 있을 때: 수신 즉시 완료
- 버퍼가 비어있을 때: 수신자 블로킹
실제 사용 예시
작업 큐 패턴
// 워커 풀을 위한 버퍼드 채널
jobs := make(chan Job, 100)
// 작업 생산자
go func() {
for i := 0; i < 1000; i++ {
jobs <- Job{ID: i}
}
close(jobs)
}()
// 작업 소비자들
for i := 0; i < 10; i++ {
go worker(jobs)
}
흐름 제어
// 동시 요청 수 제한
semaphore := make(chan struct{}, 10)
for i := 0; i < 100; i++ {
semaphore <- struct{}{} // 허용 토큰 획득
go func() {
defer func() { <-semaphore }() // 토큰 반환
// 실제 작업 수행
}()
}
Common Pitfalls and Best Practices
- 데드락 방지
- 불필요한 버퍼링 피하기
- 채널 방향성 고려
- 우아한 종료(Graceful Shutdown)
- 언블로킹을 위한
defer
사용
Channel Synchronization
채널 동기화(Channel Synchronization)
채널 동기화가 중요한 이유
- 고루틴 간 데이터가 올바르게 교환되도록 보장
- 실행 흐름을 조정하여 경쟁 상태를 방지하고 예측 가능한 동작 보장
- 고루틴의 생명주기와 작업 완료를 관리하는 데 도움
일반적인 함정과 모범 사례
- 데드락 방지
- 채널 닫기
- 불필요한 블로킹 방지
채널 동기화 패턴
완료 신호(Done Signal)
done := make(chan bool)
go func() {
// 작업 수행
fmt.Println("작업 완료")
done <- true
}()
// 완료까지 대기
<-done
fmt.Println("모든 작업 완료")
워커 풀 동기화
const numWorkers = 3
done := make(chan bool, numWorkers)
for i := 0; i < numWorkers; i++ {
go func(id int) {
// 작업 수행
fmt.Printf("워커 %d 완료\n", id)
done <- true
}(i)
}
// 모든 워커 완료 대기
for i := 0; i < numWorkers; i++ {
<-done
}
채널 닫기를 통한 종료 신호
quit := make(chan struct{})
go func() {
for {
select {
case <-quit:
fmt.Println("종료 신호 받음")
return
default:
// 계속 작업
}
}
}()
// 종료 신호 전송
close(quit)
sync.WaitGroup과의 비교
// 채널 사용
done := make(chan struct{})
go func() {
defer close(done)
// 작업 수행
}()
<-done
// WaitGroup 사용
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// 작업 수행
}()
wg.Wait()
주의사항
- 적절한 close필요
package main
import (
"fmt"
"time"
)
func main() {
data := make(chan string)
go func() {
for i := range 5 {
data <- "hello" + fmt.Sprint(i)
time.Sleep(100 * time.Millisecond)
}
close(data) // 여기서 닫아주지않으면
}()
for value := range data { // 여기서 무한 대기
fmt.Println("received value : ", value, " : ", time.Now())
}
}
- 채널을 닫은 후에는 더 이상 데이터를 전송할 수 없음
- 닫힌 채널에서 수신하면 즉시 제로값 반환
- 데드락을 피하기 위해 고루틴 생명주기를 신중하게 관리
- select문을 사용하여 논블로킹 작업 구현 가능
Channel Directions
채널 방향성이 중요한 이유
- 코드 명확성과 유지보수성 향상
- 채널에서 의도하지 않은 작업 방지
- 채널의 목적을 명확히 정의하여 타입 안전성 강화
채널 방향성의 기본 개념
- 단방향 채널(Unidirectional Channels)
- 송신 전용 채널(Send-Only Channels)
- 수신 전용 채널(Receive-Only Channels)
- 테스팅과 디버깅
함수 시그니처에서 채널 방향성 정의
- 송신 전용 매개변수:
func produceData(ch chan<- int)
- 수신 전용 매개변수:
func consumeData(ch <-chan int)
- 양방향 채널:
func bidirectional(ch chan int)
채널 방향성 사용 예시
기본 양방향 채널
// 양방향 채널 생성
ch := make(chan int)
// 송신과 수신 모두 가능
ch <- 42
value := <-ch
송신 전용 채널 함수
// 송신만 가능한 채널을 매개변수로 받음
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // 송신 가능
// value := <-ch // 컴파일 에러! 수신 불가
}
close(ch)
}
// 사용 예시
ch := make(chan int)
go producer(ch) // 양방향 채널이 송신 전용으로 변환됨
수신 전용 채널 함수
// 수신만 가능한 채널을 매개변수로 받음
func consumer(ch <-chan int) {
for value := range ch {
fmt.Println("받은 값:", value) // 수신 가능
// ch <- 100 // 컴파일 에러! 송신 불가
}
}
// 사용 예시
ch := make(chan int)
go consumer(ch) // 양방향 채널이 수신 전용으로 변환됨
func main() {
ch := make(chan int, 5)
// 생산자: 송신 전용으로 사용
go producer(ch)
// 소비자: 수신 전용으로 사용
consumer(ch)
}
func producer(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i
fmt.Printf("송신: %d\n", i)
}
close(ch)
}
func consumer(ch <-chan int) {
for value := range ch {
fmt.Printf("수신: %d\n", value)
}
}
채널 방향성 변환 규칙
ch := make(chan int) // 양방향 채널
var sendOnly chan<- int = ch // 양방향 → 송신 전용 (가능)
var recvOnly <-chan int = ch // 양방향 → 수신 전용 (가능)
// var bidir chan int = sendOnly // 송신 전용 → 양방향 (불가능!)
// var bidir chan int = recvOnly // 수신 전용 → 양방향 (불가능!)
실제 활용 시나리오
// 워커 풀 패턴에서 방향성 활용
func setupWorkerPool() {
jobs := make(chan Job, 100)
results := make(chan Result, 100)
// 작업 생성자 (송신 전용)
go jobProducer(jobs)
// 워커들 (작업은 수신, 결과는 송신)
for i := 0; i < 3; i++ {
go worker(jobs, results)
}
// 결과 수집자 (수신 전용)
resultCollector(results)
}
func jobProducer(jobs chan<- Job) {
// 작업만 송신
}
func worker(jobs <-chan Job, results chan<- Result) {
// 작업 수신, 결과 송신
}
func resultCollector(results <-chan Result) {
// 결과만 수신
}
장점과 효과
- 컴파일 타임에 채널 오용 방지
- 함수의 의도와 책임이 명확해짐
- 코드 리뷰와 유지보수가 쉬워짐
- API 설계 시 채널 사용 방법을 명확히 전달
- 실수로 인한 데드락이나 고루틴 누수 방지
Multiplexing using select
멀티플렉싱을 사용하는 이유
- 동시성(Concurrency)
- 논블로킹 작업(Non-Blocking Operations)
- 타임아웃과 취소(Timeouts and Cancellations)
select 사용 모범 사례
- 바쁜 대기 피하기(Avoiding Busy Waiting)
- 데드락 처리(Handling Deadlocks)
- 가독성과 유지보수성(Readability and Maintainability)
- 테스팅과 디버깅(Testing and Debugging)
기본 select 문법
select {
case <-ch1:
// ch1에서 수신할 때 실행
case ch2 <- value:
// ch2로 송신할 때 실행
case <-time.After(1 * time.Second):
// 1초 타임아웃
default:
// 즉시 실행 가능한 case가 없을 때
}
논블로킹 채널 작업
// 논블로킹 수신
select {
case msg := <-ch:
fmt.Println("메시지 받음:", msg)
default:
fmt.Println("메시지 없음")
}
// 논블로킹 송신
select {
case ch <- "hello":
fmt.Println("메시지 전송됨")
default:
fmt.Println("채널이 가득참")
}
여러 채널에서 동시 대기
func waitForMultipleChannels() {
ch1 := make(chan string)
ch2 := make(chan int)
quit := make(chan bool)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "문자열 데이터"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- 42
}()
for {
select {
case msg := <-ch1:
fmt.Println("ch1에서:", msg)
case num := <-ch2:
fmt.Println("ch2에서:", num)
case <-quit:
fmt.Println("종료")
return
}
}
}
타임아웃 구현
func withTimeout() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "늦은 메시지"
}()
select {
case msg := <-ch:
fmt.Println("받은 메시지:", msg)
case <-time.After(1 * time.Second):
fmt.Println("타임아웃 발생!")
}
}
워커 풀에서 select 활용
func workerWithSelect(jobs <-chan Job, results chan<- Result, quit <-chan bool) {
for {
select {
case job := <-jobs:
// 작업 처리
result := processJob(job)
results <- result
case <-quit:
fmt.Println("워커 종료")
return
}
}
}
팬인(Fan-in) 패턴
func fanIn(ch1, ch2 <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for {
select {
case msg, ok := <-ch1:
if !ok {
ch1 = nil // 닫힌 채널 무시
continue
}
out <- msg
case msg, ok := <-ch2:
if !ok {
ch2 = nil // 닫힌 채널 무시
continue
}
out <- msg
}
// 모든 채널이 닫히면 종료
if ch1 == nil && ch2 == nil {
return
}
}
}()
return out
}
select에서 우선순위 처리
func prioritySelect() {
highPriority := make(chan string)
lowPriority := make(chan string)
for {
select {
case msg := <-highPriority:
fmt.Println("높은 우선순위:", msg)
default:
select {
case msg := <-highPriority:
fmt.Println("높은 우선순위:", msg)
case msg := <-lowPriority:
fmt.Println("낮은 우선순위:", msg)
}
}
}
}
바쁜 대기 피하기
// 잘못된 예 - 바쁜 대기
for {
select {
case msg := <-ch:
processMessage(msg)
default:
// CPU를 계속 사용하게 됨
}
}
// 올바른 예 - 블로킹 대기
for {
select {
case msg := <-ch:
processMessage(msg)
case <-time.After(100 * time.Millisecond):
// 주기적인 다른 작업
doPeriodicWork()
}
}
컨텍스트를 활용한 취소
func workWithContext(ctx context.Context) {
ch := make(chan string)
go func() {
// 백그라운드 작업
time.Sleep(5 * time.Second)
ch <- "작업 완료"
}()
select {
case result := <-ch:
fmt.Println("결과:", result)
case <-ctx.Done():
fmt.Println("작업 취소됨:", ctx.Err())
}
}
주의사항과 팁
- default case가 있으면 select는 절대 블로킹되지 않음
- 여러 case가 동시에 준비되면 무작위로 선택됨
- 닫힌 채널에서 수신은 항상 즉시 실행됨 (제로값 반환)
- select 내부에서 break는 select문만 종료 (반복문 아님)
- 채널이 nil이면 해당 case는 영원히 실행되지 않음
사용예시
package main
import (
"fmt"
"time"
)
func main() {
// === NON BLOCKING RECIEVE OPERATION
ch := make(chan int)
select {
case msg := <-ch:
fmt.Println("Received: ", msg)
default:
fmt.Println("No messages available")
}
// === NON BLOCKING SEND OPERATION
select {
case ch <- 1:
fmt.Println("Sent message.")
default:
fmt.Println("Channel is not ready to receive")
}
// === NON BLOCKING OPERATION IN REAL TIME SYSTEMS
data := make(chan int)
quit := make(chan bool)
go func() {
for {
select {
case d := <-data:
fmt.Println("Data received: ", d)
case <-quit:
fmt.Println("Stopping...")
return
default:
fmt.Println("Waiting for data...")
time.Sleep(500 * time.Millisecond)
}
}
}()
for i := range 5 {
data <- i
time.Sleep(time.Second)
}
quit <- true
}
filter 패턴
package main
import "fmt"
func producer(ch chan<- int) {
for i := range 5 {
ch <- i
}
close(ch)
}
func filter(in <-chan int, out chan<- int) {
for val := range in {
if val%2 == 0 {
out <- val
}
close(out)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go producer(ch1)
go filter(ch1, ch2)
for val := range ch2 {
fmt.Println(val)
}
}
Context
Context를 사용하는 이유
- 취소(Cancellation)
- 타임아웃(Timeouts)
- 값 전달(Values)
기본 개념
Context 생성
- context.Background()
- context.TODO()
Context 계층 구조 (컨텍스트가 생성되고 파생되는 방식)
- context.WithCancel()
- context.WithDeadline()
- context.WithTimeout()
- context.WithValue()
Context 생성 방법들
context.Background()
// 최상위 컨텍스트, 취소되지 않고 값도 없음
ctx := context.Background()
// 주로 main, init, 테스트의 시작점으로 사용
func main() {
ctx := context.Background()
startApplication(ctx)
}
context.TODO()
// 어떤 컨텍스트를 사용할지 명확하지 않을 때
ctx := context.TODO()
// 리팩토링 중이거나 임시로 사용
func legacyFunction() {
ctx := context.TODO() // 나중에 적절한 컨텍스트로 교체 예정
callNewAPI(ctx)
}
취소 가능한 컨텍스트
context.WithCancel()
// 수동으로 취소할 수 있는 컨텍스트
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 리소스 정리
go func() {
// 긴 작업 수행
for {
select {
case <-ctx.Done():
fmt.Println("작업 취소됨")
return
default:
doWork()
}
}
}()
// 5초 후 취소
time.Sleep(5 * time.Second)
cancel()
실제 사용 예시: 고루틴 정리
func startWorkers(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
defer cancel() // 함수 종료 시 모든 워커 정리
for i := 0; i < 10; i++ {
go worker(ctx, i)
}
// 메인 작업 수행
time.Sleep(30 * time.Second)
// defer cancel()이 실행되어 모든 워커가 정리됨
}
func worker(ctx context.Context, id int) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
fmt.Printf("워커 %d 종료\n", id)
return
case <-ticker.C:
fmt.Printf("워커 %d 작업 중\n", id)
}
}
}
타임아웃 컨텍스트
context.WithTimeout()
// 5초 타임아웃 설정
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// API 호출
resp, err := httpClient.Get(ctx, "https://api.example.com/data")
if err != nil {
if err == context.DeadlineExceeded {
fmt.Println("API 호출 타임아웃")
}
return
}
데이터베이스 쿼리 타임아웃
func getUserData(userID int) (*User, error) {
// 3초 타임아웃으로 DB 쿼리
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := "SELECT * FROM users WHERE id = ?"
row := db.QueryRowContext(ctx, query, userID)
var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if err == context.DeadlineExceeded {
return nil, fmt.Errorf("데이터베이스 쿼리 타임아웃")
}
return nil, err
}
return &user, nil
}
데드라인 컨텍스트
context.WithDeadline()
// 특정 시각까지 실행
deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// 10분 후 자동 취소
processLongTask(ctx)
배치 작업에서 활용
func dailyBatchJob() {
// 자정까지만 실행
tomorrow := time.Now().Truncate(24*time.Hour).Add(24*time.Hour)
ctx, cancel := context.WithDeadline(context.Background(), tomorrow)
defer cancel()
for {
select {
case <-ctx.Done():
fmt.Println("배치 작업 시간 종료")
return
default:
if err := processNextBatch(ctx); err != nil {
fmt.Printf("배치 처리 실패: %v\n", err)
}
}
}
}
값 전달 컨텍스트
context.WithValue()
// 요청 ID 전달
type RequestIDKey string
const requestIDKey RequestIDKey = "requestID"
func handleRequest(w http.ResponseWriter, r *http.Request) {
requestID := generateRequestID()
// 컨텍스트에 요청 ID 저장
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
// 하위 함수들에서 요청 ID 사용 가능
processRequest(ctx)
}
func processRequest(ctx context.Context) {
// 컨텍스트에서 요청 ID 추출
requestID, ok := ctx.Value(requestIDKey).(string)
if !ok {
requestID = "unknown"
}
log.Printf("[%s] 요청 처리 시작", requestID)
// 하위 함수에도 컨텍스트 전달
callDatabase(ctx)
callExternalAPI(ctx)
}
사용자 정보 전달
type UserKey string
const userKey UserKey = "user"
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := validateToken(token)
if err != nil {
http.Error(w, "Unauthorized", 401)
return
}
// 인증된 사용자 정보를 컨텍스트에 저장
ctx := context.WithValue(r.Context(), userKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getUserProfile(w http.ResponseWriter, r *http.Request) {
// 컨텍스트에서 사용자 정보 추출
user, ok := r.Context().Value(userKey).(*User)
if !ok {
http.Error(w, "User not found in context", 500)
return
}
profile, err := getProfile(r.Context(), user.ID)
// ...
}
컨텍스트 계층 구조
복합 컨텍스트 생성
func complexRequest() {
// 1. 기본 컨텍스트
ctx := context.Background()
// 2. 타임아웃 추가 (30초)
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// 3. 요청 ID 추가
ctx = context.WithValue(ctx, "requestID", "req-123")
// 4. 사용자 정보 추가
ctx = context.WithValue(ctx, "userID", "user-456")
// 5. 취소 가능한 서브 컨텍스트
subCtx, subCancel := context.WithCancel(ctx)
defer subCancel()
// 모든 정보와 제약 조건이 하위로 전파됨
performComplexOperation(subCtx)
}
계층 구조에서 취소 전파
func demonstrateCancellationPropagation() {
// 최상위 컨텍스트
rootCtx, rootCancel := context.WithCancel(context.Background())
// 중간 계층 (10초 타임아웃)
middleCtx, middleCancel := context.WithTimeout(rootCtx, 10*time.Second)
// 하위 계층 (취소 가능)
leafCtx, leafCancel := context.WithCancel(middleCtx)
go func() {
<-leafCtx.Done()
fmt.Println("Leaf context 취소됨:", leafCtx.Err())
}()
// 5초 후 최상위 취소 → 모든 하위 컨텍스트 자동 취소
time.Sleep(5 * time.Second)
rootCancel()
// 정리
defer rootCancel()
defer middleCancel()
defer leafCancel()
}
실제 웹 서버에서의 활용
HTTP 서버 컨텍스트 체인
func main() {
mux := http.NewServeMux()
// 미들웨어 체인
handler := loggingMiddleware(
authMiddleware(
timeoutMiddleware(
businessLogicHandler(),
),
),
)
mux.Handle("/api/users", handler)
http.ListenAndServe(":8080", mux)
}
func timeoutMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 30초 타임아웃 추가
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func businessLogicHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// DB 조회 (타임아웃 적용됨)
users, err := getUsersFromDB(ctx)
if err != nil {
if err == context.DeadlineExceeded {
http.Error(w, "Request timeout", 408)
return
}
http.Error(w, "Internal error", 500)
return
}
json.NewEncoder(w).Encode(users)
})
}
모범 사례와 주의점
좋은 패턴
// 항상 첫 번째 매개변수로 전달
func goodFunction(ctx context.Context, data string) error {
return nil
}
// 컨텍스트 체크
func longRunningTask(ctx context.Context) error {
for i := 0; i < 1000; i++ {
// 주기적으로 취소 신호 확인
if err := ctx.Err(); err != nil {
return err
}
doWork()
}
return nil
}
피해야 할 패턴
// 구조체에 저장하지 말 것
type BadService struct {
ctx context.Context // 이렇게 하지 마세요
}
// nil 컨텍스트 전달하지 말 것
func badFunction(ctx context.Context) {
if ctx == nil {
ctx = context.Background() // 호출자가 해야 할 일
}
}
컨텍스트 값 사용 시 주의점
// 타입 안전한 키 사용
type contextKey string
const userIDKey contextKey = "userID"
// 타입 단언 시 안전 확인
func getUserID(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey).(string)
return userID, ok
}
// 너무 많은 값을 컨텍스트에 저장하지 말 것
// 주로 요청 범위의 메타데이터만 저장
정리
Context의 핵심 개념
- 계층적 구조로 생성 및 파생
- 상위 컨텍스트가 취소되면 모든 하위 컨텍스트도 취소
- 타임아웃, 데드라인, 값 전달 기능 제공
- 고루틴과 함수 호출 체인 전반에 걸친 생명주기 관리
주요 사용 사례
- HTTP 요청 처리 시 타임아웃 관리
- 데이터베이스 쿼리 취소
- 백그라운드 작업의 우아한 종료
- 요청 범위 메타데이터 전달 (요청 ID, 사용자 정보 등)