본문으로 건너뛰기

select 문 — 다중 채널 대기

select 문은 Go에서 여러 채널 연산을 동시에 대기할 수 있게 해주는 구문입니다. switch와 유사하지만 각 case가 채널 연산이라는 점이 다릅니다.

select 기본 문법

package main

import (
"fmt"
"time"
)

func main() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(1 * time.Second)
ch1 <- "ch1에서 온 메시지"
}()

go func() {
time.Sleep(2 * time.Second)
ch2 <- "ch2에서 온 메시지"
}()

// 두 채널 중 먼저 준비된 것 처리
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
}
}
}
// 출력:
// ch1: ch1에서 온 메시지 (1초 후)
// ch2: ch2에서 온 메시지 (1초 더 후)

select 동작 규칙:

  1. 여러 case가 동시에 준비되면 랜덤하게 하나를 선택
  2. 모든 case가 준비되지 않으면 블록 (대기)
  3. default case가 있으면 블록하지 않고 즉시 실행

timeout 패턴

네트워크 요청이나 외부 작업에 시간 제한을 둘 때 자주 사용합니다.

package main

import (
"fmt"
"time"
)

func slowOperation() <-chan string {
ch := make(chan string)
go func() {
time.Sleep(3 * time.Second) // 느린 작업 시뮬레이션
ch <- "완료"
}()
return ch
}

func main() {
result := slowOperation()

select {
case res := <-result:
fmt.Println("결과:", res)
case <-time.After(2 * time.Second): // 2초 타임아웃
fmt.Println("타임아웃! 작업이 너무 오래 걸립니다.")
}
}

time.After(d)는 지정된 시간 후에 값을 전송하는 채널을 반환합니다. 타임아웃 구현에 매우 편리합니다.

default — non-blocking 수신/전송

default case를 추가하면 select가 블록 없이 즉시 반환합니다.

package main

import "fmt"

func main() {
ch := make(chan int, 1)

// Non-blocking 전송
select {
case ch <- 42:
fmt.Println("전송 성공")
default:
fmt.Println("채널이 꽉 참, 전송 실패")
}

// Non-blocking 수신
select {
case v := <-ch:
fmt.Println("수신:", v)
default:
fmt.Println("채널이 비어 있음")
}

// 두 번째 수신 시도 (이미 비어있음)
select {
case v := <-ch:
fmt.Println("수신:", v)
default:
fmt.Println("채널이 비어 있음")
}
}
// 출력:
// 전송 성공
// 수신: 42
// 채널이 비어 있음

done 채널 패턴 — 고루틴 종료 신호

done 채널로 고루틴에 종료 신호를 보내는 패턴입니다. Go에서 가장 많이 쓰이는 패턴 중 하나입니다.

package main

import (
"fmt"
"time"
)

func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("워커 종료")
return
default:
// 실제 작업 수행
fmt.Println("작업 중...")
time.Sleep(500 * time.Millisecond)
}
}
}

func main() {
done := make(chan struct{})

go worker(done)

time.Sleep(2 * time.Second)
close(done) // 종료 신호 (값 전송이 아닌 close 사용)

time.Sleep(100 * time.Millisecond)
fmt.Println("메인 종료")
}

close(done)을 사용하면 채널을 수신하는 모든 고루틴에 동시에 신호를 보낼 수 있습니다.

우선순위 처리 패턴

중요한 작업과 일반 작업을 구분해서 처리할 때 사용합니다.

package main

import (
"fmt"
)

func prioritySelect(high, low <-chan string) {
for {
// 먼저 고우선순위 채널만 확인
select {
case msg := <-high:
fmt.Println("[HIGH]", msg)
continue
default:
}

// 고우선순위가 없으면 두 채널 모두 대기
select {
case msg := <-high:
fmt.Println("[HIGH]", msg)
case msg := <-low:
fmt.Println("[LOW]", msg)
}
}
}

func main() {
high := make(chan string, 5)
low := make(chan string, 5)

high <- "긴급 알림 1"
high <- "긴급 알림 2"
low <- "일반 작업 1"
low <- "일반 작업 2"
high <- "긴급 알림 3"

go prioritySelect(high, low)

// 결과 확인용 대기
// (실제 코드에서는 done 채널로 종료)
var input string
fmt.Scanln(&input)
}

실전 예제 — 재시도 로직과 타임아웃

package main

import (
"errors"
"fmt"
"math/rand"
"time"
)

// 가끔 실패하는 외부 서비스 시뮬레이션
func callExternalService() <-chan string {
ch := make(chan string, 1)
go func() {
delay := time.Duration(rand.Intn(3)+1) * time.Second
time.Sleep(delay)
if rand.Intn(2) == 0 {
ch <- "성공 응답"
}
// 실패 시 아무것도 전송하지 않음
}()
return ch
}

func callWithRetry(maxRetries int, timeout time.Duration) (string, error) {
for i := 0; i < maxRetries; i++ {
fmt.Printf("시도 %d/%d...\n", i+1, maxRetries)
result := callExternalService()

select {
case res := <-result:
return res, nil
case <-time.After(timeout):
fmt.Println("타임아웃, 재시도")
continue
}
}
return "", errors.New("최대 재시도 횟수 초과")
}

func main() {
rand.Seed(time.Now().UnixNano())

result, err := callWithRetry(3, 1500*time.Millisecond)
if err != nil {
fmt.Println("오류:", err)
} else {
fmt.Println("결과:", result)
}
}

select에서 nil 채널 활용

nil 채널에 대한 연산은 영원히 블록됩니다. 이를 활용해 select에서 특정 case를 동적으로 비활성화할 수 있습니다.

package main

import "fmt"

func merge(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)

go func() {
defer close(out)
for ch1 != nil || ch2 != nil {
select {
case v, ok := <-ch1:
if !ok {
ch1 = nil // 채널이 닫히면 nil로 설정 → 이 case 비활성화
continue
}
out <- v
case v, ok := <-ch2:
if !ok {
ch2 = nil
continue
}
out <- v
}
}
}()

return out
}

func main() {
ch1 := make(chan int, 3)
ch2 := make(chan int, 3)

ch1 <- 1
ch1 <- 3
ch1 <- 5
close(ch1)

ch2 <- 2
ch2 <- 4
ch2 <- 6
close(ch2)

for v := range merge(ch1, ch2) {
fmt.Printf("%d ", v)
}
fmt.Println()
}

핵심 정리

  • select— 여러 채널 연산 중 준비된 것 처리 (랜덤 선택)
  • time.After— 타임아웃 구현에 편리
  • default— non-blocking 채널 연산
  • close(done)— 여러 고루틴에 동시 종료 신호
  • nil 채널— 해당 case를 영구 블록 → 동적 비활성화에 활용