빠른 흐름
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan int)
go worker(ctx, ch)Go 동시성은 "goroutine을 만든다"보다 어디서 멈추고, 어떻게 값을 주고받고, 누가 종료를 책임지는가를 먼저 같이 봐야 합니다.
기본 흐름
goroutine은 가벼운 실행 단위다
go doWork()go 키워드를 붙이면 새 goroutine에서 함수가 실행됩니다. 하지만 이 한 줄만으로는 충분하지 않습니다. 결과를 어디로 보낼지, 언제 끝낼지, 호출자가 기다릴지까지 같이 설계해야 합니다.
channel은 goroutine 사이 값 전달 경계다
jobs := make(chan int)
results := make(chan int)channel은 공유 메모리보다 값 전달 경계로 읽는 편이 좋습니다.
func worker(jobs <-chan int, results chan<- int) {
for n := range jobs {
results <- n * 2
}
}<-chan T: 받기 전용chan<- T: 보내기 전용
방향을 시그니처에 넣으면 함수 책임이 더 선명해집니다.
context는 취소와 기한 전파를 맡는다
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()context는 결과를 담는 용도가 아니라, 취소 신호와 deadline을 전달하는 표준 경계입니다.
func worker(ctx context.Context, jobs <-chan int) {
for {
select {
case <-ctx.Done():
return
case job := <-jobs:
_ = job
}
}
}Done()이 닫히면 작업을 멈추고 빠져나오는 흐름이 기본입니다.
goroutine만 만들고 취소를 안 두면 누수가 생기기 쉽다
Go 동시성에서 흔한 실패는 goroutine을 쉽게 만드는 대신, 종료 조건을 늦게 설계하는 것입니다. 채널을 닫는 주체, context 취소 주체, 결과를 소비하지 못할 때의 처리까지 초반에 같이 두는 편이 좋습니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 작업 하나를 병렬로 돌린다 | goroutine |
| goroutine 사이 값을 주고받는다 | channel |
| 취소/timeout을 전파한다 | context.Context |
| 함수 책임을 시그니처로 드러낸다 | 방향 있는 channel |
| 작업 종료를 호출자가 책임진다 | cancel() 또는 채널 종료 주체 명시 |
주의할 점
Go에서 goroutine은 만들기 쉽지만, 자동으로 정리되지는 않습니다. 결과를 읽지 않는 채널, 닫히지 않는 작업 채널, 호출되지 않는 cancel()은 goroutine 누수로 바로 이어질 수 있습니다. go 키워드를 붙이는 순간, 종료 조건과 소유권도 같이 적는 습관이 필요합니다.
참고 링크
3 sources