⚡ GO 동시성 - 고루틴과 채널

📋 목차


1. 고루틴 기초

🚀 고루틴이란?

고루틴 (Goroutine)

고루틴은 Go 런타임에서 관리하는 경량 스레드입니다. OS 스레드보다 훨씬 가볍고 빠르며, 수천 개를 동시에 실행할 수 있습니다.

package main
 
import (
    "fmt"
    "time"
    "runtime"
)
 
// 일반 함수
func sayHello(name string) {
    for i := 0; i < 5; i++ {
        fmt.Printf("Hello from %s: %d\n", name, i)
        time.Sleep(100 * time.Millisecond)
    }
}
 
// 고루틴으로 실행할 함수
func sayHelloGoroutine(name string) {
    for i := 0; i < 5; i++ {
        fmt.Printf("[Goroutine] Hello from %s: %d\n", name, i)
        time.Sleep(100 * time.Millisecond)
    }
}
 
func goroutineBasics() {
    fmt.Printf("Go version: %s\n", runtime.Version())
    fmt.Printf("CPU cores: %d\n", runtime.NumCPU())
    fmt.Printf("Initial goroutines: %d\n", runtime.NumGoroutine())
    
    // 일반 함수 호출 (순차 실행)
    fmt.Println("\n=== Sequential Execution ===")
    sayHello("Alice")
    sayHello("Bob")
    
    // 고루틴으로 실행 (동시 실행)
    fmt.Println("\n=== Concurrent Execution ===")
    go sayHelloGoroutine("Charlie")  // 고루틴으로 실행
    go sayHelloGoroutine("Diana")    // 고루틴으로 실행
    
    fmt.Printf("After launching goroutines: %d\n", runtime.NumGoroutine())
    
    // 고루틴이 완료될 때까지 대기
    time.Sleep(1 * time.Second)
    
    fmt.Printf("Final goroutines: %d\n", runtime.NumGoroutine())
}

🎭 익명 함수와 고루틴

func anonymousGoroutines() {
    fmt.Println("=== Anonymous Goroutines ===")
    
    // 익명 함수를 고루틴으로 실행
    go func() {
        fmt.Println("Anonymous goroutine 1")
        time.Sleep(200 * time.Millisecond)
        fmt.Println("Anonymous goroutine 1 done")
    }()
    
    // 매개변수가 있는 익명 함수
    go func(message string, count int) {
        for i := 0; i < count; i++ {
            fmt.Printf("Anonymous goroutine 2: %s %d\n", message, i)
            time.Sleep(150 * time.Millisecond)
        }
    }("Hello", 3)
    
    // 클로저 사용 (주의: 변수 캡처)
    for i := 0; i < 3; i++ {
        // 잘못된 예시 - 모든 고루틴이 같은 i 값을 참조
        go func() {
            fmt.Printf("Wrong closure: %d\n", i)  // 모두 3이 출력될 수 있음
        }()
        
        // 올바른 예시 - 매개변수로 값 전달
        go func(num int) {
            fmt.Printf("Correct closure: %d\n", num)
        }(i)
        
        // 또 다른 올바른 예시 - 지역 변수 생성
        i := i  // 지역 변수로 복사
        go func() {
            fmt.Printf("Local copy: %d\n", i)
        }()
    }
    
    time.Sleep(1 * time.Second)
}

🕐 고루틴 라이프사이클

import (
    "context"
    "sync"
)
 
func goroutineLifecycle() {
    var wg sync.WaitGroup
    
    // 1. 정상 완료되는 고루틴
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println("Task 1: Starting")
        time.Sleep(500 * time.Millisecond)
        fmt.Println("Task 1: Completed")
    }()
    
    // 2. 컨텍스트로 취소되는 고루틴
    ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
    defer cancel()
    
    wg.Add(1)
    go func() {
        defer wg.Done()
        
        select {
        case <-time.After(1 * time.Second):
            fmt.Println("Task 2: Completed")
        case <-ctx.Done():
            fmt.Println("Task 2: Cancelled due to timeout")
        }
    }()
    
    // 3. 채널을 통한 종료 신호
    quit := make(chan bool)
    
    wg.Add(1)
    go func() {
        defer wg.Done()
        
        for {
            select {
            case <-quit:
                fmt.Println("Task 3: Received quit signal")
                return
            default:
                fmt.Println("Task 3: Working...")
                time.Sleep(100 * time.Millisecond)
            }
        }
    }()
    
    // 200ms 후 종료 신호 전송
    time.Sleep(200 * time.Millisecond)
    quit <- true
    
    // 모든 고루틴 완료 대기
    wg.Wait()
    fmt.Println("All tasks completed")
}

2. 채널 기초

📡 채널이란?

채널 (Channel)

채널은 고루틴 간 통신을 위한 파이프입니다. 타입 안전하며, 동기화 기능도 제공합니다.

func channelBasics() {
    // 1. 채널 생성
    messages := make(chan string)     // unbuffered 채널
    numbers := make(chan int, 3)      // buffered 채널 (크기 3)
    
    // 2. 고루틴에서 채널로 데이터 전송
    go func() {
        messages <- "Hello"           // 채널에 값 전송 (blocked until received)
        messages <- "World"
        close(messages)               // 채널 닫기
    }()
    
    // 3. 채널에서 데이터 수신
    for message := range messages {   // 채널이 닫힐 때까지 수신
        fmt.Println("Received:", message)
    }
    
    // 4. 버퍼드 채널 사용
    go func() {
        for i := 1; i <= 5; i++ {
            numbers <- i
            fmt.Printf("Sent: %d (buffer size: 3)\n", i)
        }
        close(numbers)
    }()
    
    // 5. 채널 수신 패턴들
    
    // 패턴 1: range로 모든 값 수신
    for num := range numbers {
        fmt.Printf("Received: %d\n", num)
        time.Sleep(200 * time.Millisecond)  // 처리 시간 시뮬레이션
    }
    
    // 패턴 2: ok 변수로 채널 상태 확인
    testChannel := make(chan int, 1)
    testChannel <- 42
    close(testChannel)
    
    if value, ok := <-testChannel; ok {
        fmt.Printf("Channel is open, received: %d\n", value)
    } else {
        fmt.Println("Channel is closed")
    }
    
    if value, ok := <-testChannel; ok {
        fmt.Printf("Channel is open, received: %d\n", value)
    } else {
        fmt.Println("Channel is closed (no more values)")
    }
}

🎛️ 채널 방향성

// 채널 방향성 지정
func channelDirections() {
    messages := make(chan string)
    
    // send-only 채널로 전달
    go sender(messages)
    
    // receive-only 채널로 전달
    go receiver(messages)
    
    time.Sleep(1 * time.Second)
}
 
// send-only 채널 (chan<- string)
func sender(ch chan<- string) {
    messages := []string{"Hello", "Go", "Channels"}
    
    for _, message := range messages {
        ch <- message
        fmt.Printf("Sent: %s\n", message)
        time.Sleep(200 * time.Millisecond)
    }
    
    close(ch)
}
 
// receive-only 채널 (<-chan string)
func receiver(ch <-chan string) {
    for message := range ch {
        fmt.Printf("Received: %s\n", message)
    }
    fmt.Println("Receiver finished")
}

🔄 select 문

select 문

select는 여러 채널 작업 중 실행 가능한 것을 선택하는 Go의 특별한 제어문입니다.

func selectBasics() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    quit := make(chan bool)
    
    // 고루틴 1: ch1로 데이터 전송
    go func() {
        time.Sleep(200 * time.Millisecond)
        ch1 <- "Message from channel 1"
    }()
    
    // 고루틴 2: ch2로 데이터 전송
    go func() {
        time.Sleep(100 * time.Millisecond)
        ch2 <- "Message from channel 2"
    }()
    
    // 고루틴 3: 타임아웃 후 종료 신호
    go func() {
        time.Sleep(500 * time.Millisecond)
        quit <- true
    }()
    
    // select로 여러 채널 동시 대기
    for {
        select {
        case msg1 := <-ch1:
            fmt.Printf("From ch1: %s\n", msg1)
            
        case msg2 := <-ch2:
            fmt.Printf("From ch2: %s\n", msg2)
            
        case <-quit:
            fmt.Println("Quit signal received")
            return
            
        case <-time.After(1 * time.Second):
            fmt.Println("Timeout - no message received")
            return
            
        default:
            fmt.Println("No channel is ready")
            time.Sleep(50 * time.Millisecond)
        }
    }
}

🎯 채널을 활용한 패턴들

// 1. Fan-out: 하나의 입력을 여러 고루틴으로 분산
func fanOut(input <-chan int) (<-chan int, <-chan int) {
    out1 := make(chan int)
    out2 := make(chan int)
    
    go func() {
        defer close(out1)
        defer close(out2)
        
        for val := range input {
            // 두 채널로 동시에 전송
            out1 <- val
            out2 <- val
        }
    }()
    
    return out1, out2
}
 
// 2. 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  // 닫힌 채널을 nil로 설정해서 select에서 제외
                } else {
                    out <- fmt.Sprintf("Ch1: %s", msg)
                }
            case msg, ok := <-ch2:
                if !ok {
                    ch2 = nil
                } else {
                    out <- fmt.Sprintf("Ch2: %s", msg)
                }
            }
            
            // 두 채널이 모두 닫히면 종료
            if ch1 == nil && ch2 == nil {
                break
            }
        }
    }()
    
    return out
}
 
// 3. Worker Pool 패턴
func workerPool() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    
    // 워커 3개 시작
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    
    // 작업 전송
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)
    
    // 결과 수집
    for r := 1; r <= 9; r++ {
        result := <-results
        fmt.Printf("Result: %d\n", result)
    }
}
 
func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job)
        time.Sleep(200 * time.Millisecond)  // 작업 시뮬레이션
        results <- job * job
    }
}
 
func channelPatterns() {
    fmt.Println("=== Fan-out Pattern ===")
    input := make(chan int, 3)
    
    // 입력 데이터 생성
    go func() {
        defer close(input)
        for i := 1; i <= 3; i++ {
            input <- i
        }
    }()
    
    out1, out2 := fanOut(input)
    
    // 두 출력 채널에서 데이터 수신
    go func() {
        for val := range out1 {
            fmt.Printf("Out1 received: %d\n", val)
        }
    }()
    
    go func() {
        for val := range out2 {
            fmt.Printf("Out2 received: %d\n", val)
        }
    }()
    
    time.Sleep(500 * time.Millisecond)
    
    fmt.Println("\n=== Fan-in Pattern ===")
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        defer close(ch1)
        for i := 1; i <= 3; i++ {
            ch1 <- fmt.Sprintf("Message %d", i)
            time.Sleep(100 * time.Millisecond)
        }
    }()
    
    go func() {
        defer close(ch2)
        for i := 1; i <= 3; i++ {
            ch2 <- fmt.Sprintf("Data %d", i)
            time.Sleep(150 * time.Millisecond)
        }
    }()
    
    merged := fanIn(ch1, ch2)
    for msg := range merged {
        fmt.Printf("Merged: %s\n", msg)
    }
    
    fmt.Println("\n=== Worker Pool Pattern ===")
    workerPool()
}

3. 채널 고급 패턴

🎪 Pipeline 패턴

// 파이프라인 단계별 함수들
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, num := range nums {
            out <- num
        }
    }()
    return out
}
 
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for num := range in {
            out <- num * num
        }
    }()
    return out
}
 
func filter(in <-chan int, predicate func(int) bool) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for num := range in {
            if predicate(num) {
                out <- num
            }
        }
    }()
    return out
}
 
func pipelineExample() {
    fmt.Println("=== Pipeline Pattern ===")
    
    // 파이프라인 구성: generate -> square -> filter
    numbers := generate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    squared := square(numbers)
    evens := filter(squared, func(n int) bool { return n%2 == 0 })
    
    // 결과 출력
    for result := range evens {
        fmt.Printf("Even square: %d\n", result)
    }
}

🔄 Pub/Sub 패턴

import "sync"
 
type PubSub struct {
    mu          sync.RWMutex
    subscribers map[string][]chan<- string
}
 
func NewPubSub() *PubSub {
    return &PubSub{
        subscribers: make(map[string][]chan<- string),
    }
}
 
func (ps *PubSub) Subscribe(topic string) <-chan string {
    ps.mu.Lock()
    defer ps.mu.Unlock()
    
    ch := make(chan string, 1)
    ps.subscribers[topic] = append(ps.subscribers[topic], ch)
    
    return ch
}
 
func (ps *PubSub) Publish(topic, message string) {
    ps.mu.RLock()
    defer ps.mu.RUnlock()
    
    for _, ch := range ps.subscribers[topic] {
        // Non-blocking send
        select {
        case ch <- message:
        default:
            fmt.Printf("Subscriber buffer full, dropping message: %s\n", message)
        }
    }
}
 
func (ps *PubSub) Unsubscribe(topic string, ch <-chan string) {
    ps.mu.Lock()
    defer ps.mu.Unlock()
    
    subscribers := ps.subscribers[topic]
    for i, subscriber := range subscribers {
        if subscriber == ch {
            // Remove from slice
            ps.subscribers[topic] = append(subscribers[:i], subscribers[i+1:]...)
            close(subscriber)
            break
        }
    }
}
 
func pubSubExample() {
    fmt.Println("=== Pub/Sub Pattern ===")
    
    pubsub := NewPubSub()
    
    // 구독자 1: 뉴스 토픽
    newsSubscriber1 := pubsub.Subscribe("news")
    go func() {
        for msg := range newsSubscriber1 {
            fmt.Printf("News Subscriber 1: %s\n", msg)
        }
    }()
    
    // 구독자 2: 뉴스 토픽
    newsSubscriber2 := pubsub.Subscribe("news")
    go func() {
        for msg := range newsSubscriber2 {
            fmt.Printf("News Subscriber 2: %s\n", msg)
        }
    }()
    
    // 구독자 3: 스포츠 토픽
    sportsSubscriber := pubsub.Subscribe("sports")
    go func() {
        for msg := range sportsSubscriber {
            fmt.Printf("Sports Subscriber: %s\n", msg)
        }
    }()
    
    // 메시지 발행
    time.Sleep(100 * time.Millisecond)  // 구독자 준비 시간
    
    pubsub.Publish("news", "Breaking: Go 1.22 Released!")
    pubsub.Publish("sports", "Football: Team A wins 3-0")
    pubsub.Publish("news", "Tech: New AI Model Announced")
    pubsub.Publish("weather", "Sunny day ahead")  // 구독자 없음
    
    time.Sleep(500 * time.Millisecond)
}

⚡ Rate Limiting 패턴

import "golang.org/x/time/rate"
 
// 토큰 버킷을 이용한 Rate Limiter
type TokenBucket struct {
    tokens chan struct{}
    ticker *time.Ticker
}
 
func NewTokenBucket(capacity int, refillRate time.Duration) *TokenBucket {
    tb := &TokenBucket{
        tokens: make(chan struct{}, capacity),
        ticker: time.NewTicker(refillRate),
    }
    
    // 초기 토큰 채우기
    for i := 0; i < capacity; i++ {
        tb.tokens <- struct{}{}
    }
    
    // 주기적으로 토큰 보충
    go func() {
        for range tb.ticker.C {
            select {
            case tb.tokens <- struct{}{}:
            default:
                // 버킷이 가득 차면 무시
            }
        }
    }()
    
    return tb
}
 
func (tb *TokenBucket) TakeToken() bool {
    select {
    case <-tb.tokens:
        return true
    default:
        return false
    }
}
 
func (tb *TokenBucket) Stop() {
    tb.ticker.Stop()
}
 
// Rate limiter를 사용한 API 호출 시뮬레이션
func rateLimitingExample() {
    fmt.Println("=== Rate Limiting Pattern ===")
    
    // 1초에 3개 토큰, 최대 5개 토큰 보유 가능
    limiter := NewTokenBucket(5, 333*time.Millisecond)
    defer limiter.Stop()
    
    // 10개의 API 요청 시뮬레이션
    for i := 1; i <= 10; i++ {
        if limiter.TakeToken() {
            fmt.Printf("API Request %d: Allowed\n", i)
            
            // API 호출 시뮬레이션
            go func(reqID int) {
                time.Sleep(100 * time.Millisecond)
                fmt.Printf("API Request %d: Completed\n", reqID)
            }(i)
        } else {
            fmt.Printf("API Request %d: Rate limited\n", i)
        }
        
        time.Sleep(200 * time.Millisecond)  // 요청 간격
    }
    
    time.Sleep(2 * time.Second)
    
    // Go의 rate 패키지 사용 예제
    fmt.Println("\n=== Using golang.org/x/time/rate ===")
    
    // 초당 2개, 버스트 5개
    rl := rate.NewLimiter(rate.Limit(2), 5)
    
    for i := 1; i <= 8; i++ {
        if rl.Allow() {
            fmt.Printf("Request %d: Allowed\n", i)
        } else {
            fmt.Printf("Request %d: Rate limited\n", i)
        }
        time.Sleep(300 * time.Millisecond)
    }
}

4. 동기화 도구

🔒 Mutex와 RWMutex

import (
    "math/rand"
    "sync"
)
 
type SafeCounter struct {
    mu    sync.Mutex
    value int
}
 
func (sc *SafeCounter) Increment() {
    sc.mu.Lock()
    defer sc.mu.Unlock()
    sc.value++
}
 
func (sc *SafeCounter) GetValue() int {
    sc.mu.Lock()
    defer sc.mu.Unlock()
    return sc.value
}
 
type SafeMap struct {
    mu   sync.RWMutex
    data map[string]int
}
 
func NewSafeMap() *SafeMap {
    return &SafeMap{
        data: make(map[string]int),
    }
}
 
func (sm *SafeMap) Set(key string, value int) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.data[key] = value
}
 
func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.RLock()  // 읽기 전용 락
    defer sm.mu.RUnlock()
    value, ok := sm.data[key]
    return value, ok
}
 
func (sm *SafeMap) GetAll() map[string]int {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    
    // 복사본 반환
    result := make(map[string]int)
    for k, v := range sm.data {
        result[k] = v
    }
    return result
}
 
func mutexExample() {
    fmt.Println("=== Mutex Example ===")
    
    counter := &SafeCounter{}
    var wg sync.WaitGroup
    
    // 10개 고루틴이 동시에 카운터 증가
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 100; j++ {
                counter.Increment()
            }
            fmt.Printf("Goroutine %d finished\n", id)
        }(i)
    }
    
    wg.Wait()
    fmt.Printf("Final counter value: %d\n", counter.GetValue())
    
    fmt.Println("\n=== RWMutex Example ===")
    
    safeMap := NewSafeMap()
    
    // 데이터 쓰기 고루틴
    wg.Add(1)
    go func() {
        defer wg.Done()
        for i := 0; i < 5; i++ {
            key := fmt.Sprintf("key%d", i)
            value := rand.Intn(100)
            safeMap.Set(key, value)
            fmt.Printf("Set %s = %d\n", key, value)
            time.Sleep(200 * time.Millisecond)
        }
    }()
    
    // 데이터 읽기 고루틴들
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 10; j++ {
                all := safeMap.GetAll()
                fmt.Printf("Reader %d: %v\n", id, all)
                time.Sleep(100 * time.Millisecond)
            }
        }(i)
    }
    
    wg.Wait()
}

🎯 sync.Once와 sync.Pool

type Singleton struct {
    value string
}
 
var (
    instance *Singleton
    once     sync.Once
)
 
func GetSingleton() *Singleton {
    once.Do(func() {
        fmt.Println("Creating singleton instance")
        instance = &Singleton{value: "I am singleton"}
    })
    return instance
}
 
func syncOnceExample() {
    fmt.Println("=== sync.Once Example ===")
    
    var wg sync.WaitGroup
    
    // 여러 고루틴이 동시에 싱글톤 접근
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            s := GetSingleton()
            fmt.Printf("Goroutine %d: %s\n", id, s.value)
        }(i)
    }
    
    wg.Wait()
}
 
// sync.Pool 예제
type ExpensiveObject struct {
    data [1000]byte
}
 
var objectPool = sync.Pool{
    New: func() interface{} {
        fmt.Println("Creating new expensive object")
        return &ExpensiveObject{}
    },
}
 
func useExpensiveObject() {
    // 풀에서 객체 가져오기
    obj := objectPool.Get().(*ExpensiveObject)
    
    // 사용
    fmt.Println("Using expensive object")
    time.Sleep(100 * time.Millisecond)
    
    // 다시 풀에 반환
    objectPool.Put(obj)
}
 
func syncPoolExample() {
    fmt.Println("\n=== sync.Pool Example ===")
    
    var wg sync.WaitGroup
    
    // 여러 고루틴이 객체 사용
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Goroutine %d ", id)
            useExpensiveObject()
        }(i)
    }
    
    wg.Wait()
}

🎯 실습 예제

📝 연습 문제 1: 웹 크롤러

package main
 
import (
    "fmt"
    "net/http"
    "sync"
    "time"
)
 
type CrawlResult struct {
    URL        string
    StatusCode int
    Error      error
    Duration   time.Duration
}
 
type WebCrawler struct {
    maxWorkers   int
    visitedMutex sync.Mutex
    visited      map[string]bool
    results      chan CrawlResult
}
 
func NewWebCrawler(maxWorkers int) *WebCrawler {
    return &WebCrawler{
        maxWorkers: maxWorkers,
        visited:    make(map[string]bool),
        results:    make(chan CrawlResult),
    }
}
 
func (wc *WebCrawler) isVisited(url string) bool {
    wc.visitedMutex.Lock()
    defer wc.visitedMutex.Unlock()
    
    if wc.visited[url] {
        return true
    }
    wc.visited[url] = true
    return false
}
 
func (wc *WebCrawler) crawlURL(url string) {
    if wc.isVisited(url) {
        return
    }
    
    start := time.Now()
    resp, err := http.Get(url)
    duration := time.Since(start)
    
    result := CrawlResult{
        URL:      url,
        Duration: duration,
        Error:    err,
    }
    
    if err == nil {
        result.StatusCode = resp.StatusCode
        resp.Body.Close()
    }
    
    wc.results <- result
}
 
func (wc *WebCrawler) Crawl(urls []string) []CrawlResult {
    jobs := make(chan string, len(urls))
    var wg sync.WaitGroup
    
    // 워커 시작
    for i := 0; i < wc.maxWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for url := range jobs {
                wc.crawlURL(url)
            }
        }()
    }
    
    // 결과 수집 고루틴
    var results []CrawlResult
    done := make(chan bool)
    go func() {
        for result := range wc.results {
            results = append(results, result)
            if len(results) == len(urls) {
                done <- true
                break
            }
        }
    }()
    
    // URL 작업 전송
    for _, url := range urls {
        jobs <- url
    }
    close(jobs)
    
    // 완료 대기
    <-done
    wg.Wait()
    close(wc.results)
    
    return results
}
 
func webCrawlerExample() {
    fmt.Println("=== Web Crawler Example ===")
    
    urls := []string{
        "https://golang.org",
        "https://github.com",
        "https://stackoverflow.com",
        "https://reddit.com",
        "https://news.ycombinator.com",
        "https://invalid-url-that-should-fail.com",
    }
    
    crawler := NewWebCrawler(3)  // 3개 워커
    results := crawler.Crawl(urls)
    
    fmt.Printf("\nCrawl Results (%d URLs):\n", len(results))
    fmt.Println("----------------------------------------")
    
    for _, result := range results {
        status := fmt.Sprintf("Status: %d", result.StatusCode)
        if result.Error != nil {
            status = fmt.Sprintf("Error: %v", result.Error)
        }
        
        fmt.Printf("%-30s | %-20s | Duration: %v\n", 
            result.URL, status, result.Duration)
    }
}

📝 연습 문제 2: 실시간 로그 프로세서

import (
    "bufio"
    "regexp"
    "strings"
)
 
type LogEntry struct {
    Timestamp string
    Level     string
    Message   string
    Raw       string
}
 
type LogProcessor struct {
    logPattern *regexp.Regexp
    filters    []LogFilter
    processors []chan LogEntry
    stats      *LogStats
}
 
type LogFilter func(LogEntry) bool
 
type LogStats struct {
    mu                    sync.RWMutex
    totalLogs            int
    logsByLevel          map[string]int
    errorPatterns        map[string]int
}
 
func NewLogStats() *LogStats {
    return &LogStats{
        logsByLevel:   make(map[string]int),
        errorPatterns: make(map[string]int),
    }
}
 
func (ls *LogStats) Record(entry LogEntry) {
    ls.mu.Lock()
    defer ls.mu.Unlock()
    
    ls.totalLogs++
    ls.logsByLevel[entry.Level]++
    
    // 에러 패턴 감지
    if entry.Level == "ERROR" {
        if strings.Contains(entry.Message, "database") {
            ls.errorPatterns["database"]++
        } else if strings.Contains(entry.Message, "network") {
            ls.errorPatterns["network"]++
        } else if strings.Contains(entry.Message, "timeout") {
            ls.errorPatterns["timeout"]++
        }
    }
}
 
func (ls *LogStats) GetStats() map[string]interface{} {
    ls.mu.RLock()
    defer ls.mu.RUnlock()
    
    return map[string]interface{}{
        "total_logs":     ls.totalLogs,
        "logs_by_level":  copyMap(ls.logsByLevel),
        "error_patterns": copyMap(ls.errorPatterns),
    }
}
 
func copyMap(m map[string]int) map[string]int {
    result := make(map[string]int)
    for k, v := range m {
        result[k] = v
    }
    return result
}
 
func NewLogProcessor() *LogProcessor {
    // 로그 패턴: 2025-11-26 10:30:45 [INFO] Message
    pattern := regexp.MustCompile(`(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (.+)`)
    
    return &LogProcessor{
        logPattern: pattern,
        stats:      NewLogStats(),
    }
}
 
func (lp *LogProcessor) AddFilter(filter LogFilter) {
    lp.filters = append(lp.filters, filter)
}
 
func (lp *LogProcessor) AddProcessor(processor chan LogEntry) {
    lp.processors = append(lp.processors, processor)
}
 
func (lp *LogProcessor) ParseLog(line string) *LogEntry {
    matches := lp.logPattern.FindStringSubmatch(line)
    if len(matches) != 4 {
        return nil
    }
    
    return &LogEntry{
        Timestamp: matches[1],
        Level:     matches[2],
        Message:   matches[3],
        Raw:       line,
    }
}
 
func (lp *LogProcessor) ProcessLogs(logLines []string) {
    var wg sync.WaitGroup
    
    // 프로세서 고루틴들 시작
    for i, processor := range lp.processors {
        wg.Add(1)
        go func(id int, ch chan LogEntry) {
            defer wg.Done()
            fmt.Printf("Processor %d started\n", id)
            
            for entry := range ch {
                // 프로세서별 로직 (여기서는 출력만)
                fmt.Printf("Processor %d: [%s] %s - %s\n", 
                    id, entry.Level, entry.Timestamp, entry.Message)
                time.Sleep(50 * time.Millisecond)  // 처리 시뮬레이션
            }
            
            fmt.Printf("Processor %d finished\n", id)
        }(i, processor)
    }
    
    // 로그 처리
    for _, line := range logLines {
        entry := lp.ParseLog(line)
        if entry == nil {
            continue
        }
        
        // 필터 적용
        passed := true
        for _, filter := range lp.filters {
            if !filter(*entry) {
                passed = false
                break
            }
        }
        
        if !passed {
            continue
        }
        
        // 통계 기록
        lp.stats.Record(*entry)
        
        // 모든 프로세서에 전송
        for _, processor := range lp.processors {
            processor <- *entry
        }
    }
    
    // 프로세서 채널 닫기
    for _, processor := range lp.processors {
        close(processor)
    }
    
    wg.Wait()
}
 
func logProcessorExample() {
    fmt.Println("=== Log Processor Example ===")
    
    // 샘플 로그 데이터
    logLines := []string{
        "2025-11-26 10:30:45 [INFO] Application started",
        "2025-11-26 10:30:46 [DEBUG] Loading configuration",
        "2025-11-26 10:30:47 [INFO] Database connected",
        "2025-11-26 10:30:48 [WARN] High memory usage detected",
        "2025-11-26 10:30:49 [ERROR] Database connection timeout",
        "2025-11-26 10:30:50 [ERROR] Network error: connection refused",
        "2025-11-26 10:30:51 [INFO] User logged in",
        "2025-11-26 10:30:52 [ERROR] Database query failed",
        "Invalid log line",
        "2025-11-26 10:30:53 [INFO] Request processed successfully",
    }
    
    processor := NewLogProcessor()
    
    // 필터 추가: DEBUG 레벨 제외
    processor.AddFilter(func(entry LogEntry) bool {
        return entry.Level != "DEBUG"
    })
    
    // 프로세서 추가
    errorProcessor := make(chan LogEntry, 10)
    infoProcessor := make(chan LogEntry, 10)
    
    processor.AddProcessor(errorProcessor)
    processor.AddProcessor(infoProcessor)
    
    // 로그 처리 실행
    processor.ProcessLogs(logLines)
    
    // 통계 출력
    fmt.Println("\n=== Log Statistics ===")
    stats := processor.stats.GetStats()
    
    fmt.Printf("Total logs processed: %v\n", stats["total_logs"])
    fmt.Printf("Logs by level: %v\n", stats["logs_by_level"])
    fmt.Printf("Error patterns: %v\n", stats["error_patterns"])
}

✅ 체크리스트

고루틴

  • 고루틴 기본 개념과 생성
  • 익명 함수와 고루틴
  • 클로저와 변수 캡처 주의사항
  • 고루틴 생명주기 관리

채널

  • 채널 기본 사용법
  • 버퍼드 vs 언버퍼드 채널
  • 채널 방향성 지정
  • select 문 활용

동시성 패턴

  • Fan-out/Fan-in 패턴
  • Worker Pool 패턴
  • Pipeline 패턴
  • Pub/Sub 패턴

동기화

  • Mutex와 RWMutex
  • sync.Once와 sync.Pool
  • WaitGroup 사용법
  • Context를 통한 취소

다음 단계

동시성을 익혔다면 GO 실전 REST API 프로젝트로 넘어가세요!