Go CSP 모델

가서 포옹해라 순차 프로세스 통신(CSP), 정식 모델 1978년 Tony Hoare가 제안했습니다. 기본 원칙: 여러 개의 고루틴을 사용하는 대신 동일한 변수(뮤텍스 사용)를 읽고 쓰는 경우, 고루틴은 다음을 통해 통신합니다. 채널 — 데이터 소유권을 전송하는 형식화된 파이프입니다.

Go 런타임은 OS 스레드 풀에서 고루틴 일정을 관리합니다(기본적으로 사용 가능한 코어 수에 따라 제어됨 GOMAXPROCS). 고루틴은 차단 작업(I/O, 채널 수신) 중에는 자동으로 중단되어 다른 작업을 허용합니다. 고루틴을 진행합니다.

고루틴: 구조 및 수명주기

package main

import (
    "fmt"
    "time"
)

func worker(id int, done chan struct{}) {
    fmt.Printf("Worker %d started\n", id)
    time.Sleep(100 * time.Millisecond)     // simula lavoro
    fmt.Printf("Worker %d finished\n", id)
    done <- struct{}{}                       // segnala completamento
}

func main() {
    done := make(chan struct{}, 5)  // channel bufferizzato per 5

    // Lancia 5 goroutine concorrentemente
    for i := 0; i < 5; i++ {
        go worker(i, done)           // "go" avvia la goroutine
    }

    // Aspetta che tutte completino
    for i := 0; i < 5; i++ {
        <-done                       // riceve dal channel (blocca se vuoto)
    }

    fmt.Println("All workers done")
}

// Costo di una goroutine: ~2-8KB stack (cresce dinamicamente fino a 1GB)
// vs ~1MB per un OS thread
// Go può avere milioni di goroutine attive contemporaneamente

채널: 유형 및 패턴

버퍼링되지 않은 채널과 버퍼링된 채널

// Channel unbuffered: sincronizzazione diretta
// Send blocca finché un receiver è pronto
ch := make(chan int)         // unbuffered
go func() { ch <- 42 }()  // blocca finché qualcuno riceve
v := <-ch                   // sblocca il sender

// Channel buffered: coda FIFO con capacità fissata
// Send blocca SOLO se il buffer è pieno
buffered := make(chan int, 10)   // buffer da 10 elementi
buffered <- 1                   // non blocca (buffer ha spazio)
buffered <- 2                   // non blocca
v := <-buffered                 // riceve 1 (FIFO)

// Directional channel types: sicurezza a compile time
func producer(ch chan<- int) { // solo write
    ch <- 42
}

func consumer(ch <-chan int) { // solo read
    v := <-ch
    fmt.Println(v)
}

패턴: 파이프라인

// Pipeline: catena di goroutine connesse da channel
// Ogni fase legge dall'input channel e scrive sull'output channel

func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)    // IMPORTANTE: chiudi quando finisci
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {  // range su channel riceve finché chiuso
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    // Compone la pipeline
    c := generate(2, 3, 4, 5)
    out := square(c)

    // Consuma l'output
    for v := range out {
        fmt.Println(v)  // 4, 9, 16, 25
    }
}

선택: 다중 채널에서의 다중화

select Go에서 경쟁을 위한 가장 강력한 키워드는 기다림입니다. 여러 채널을 동시에 실행하고 채널 준비 케이스를 실행합니다. 채널이 여러 개인 경우 준비가 되면 그는 무작위로 선택합니다.

// Timeout pattern con select
import "time"

func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
    resultCh := make(chan string, 1)
    errCh := make(chan error, 1)

    go func() {
        result, err := fetch(url)
        if err != nil {
            errCh <- err
            return
        }
        resultCh <- result
    }()

    select {
    case result := <-resultCh:
        return result, nil
    case err := <-errCh:
        return "", err
    case <-time.After(timeout):
        return "", fmt.Errorf("timeout after %v", timeout)
    }
}

// Fan-out/fan-in con select e done channel
func merge(channels ...<-chan int) <-chan int {
    merged := make(chan int)
    var wg sync.WaitGroup

    multiplex := func(ch <-chan int) {
        defer wg.Done()
        for v := range ch {
            merged <- v
        }
    }

    wg.Add(len(channels))
    for _, ch := range channels {
        go multiplex(ch)
    }

    go func() {
        wg.Wait()
        close(merged)
    }()

    return merged
}

context.Context: 전파된 삭제

패키지 context 삭제 전파를 위한 Go의 표준 메커니즘입니다. 고루틴 체인을 통해. I/O나 긴 작업을 수행하는 모든 함수는 이를 허용해야 합니다. 에 context.Context 첫 번째 매개변수로:

import (
    "context"
    "fmt"
    "time"
)

// Funzione che rispetta la cancellazione
func longOperation(ctx context.Context, id int) error {
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():              // controlla se il context è cancellato
            return ctx.Err()           // errore: context.Canceled o DeadlineExceeded
        default:
            fmt.Printf("Step %d/%d\n", i+1, 10)
            time.Sleep(100 * time.Millisecond)
        }
    }
    return nil
}

func main() {
    // Context con timeout di 500ms
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()  // SEMPRE defer cancel() per liberare le risorse

    err := longOperation(ctx, 1)
    if err != nil {
        fmt.Printf("Cancelled: %v\n", err)  // context deadline exceeded
    }
}

// Context si propaga attraverso le chiamate:
// Handler HTTP -> Service -> Repository -> Database
// Se il client disconnette, il context si cancella e risale tutta la catena

WaitGroup: 동기화 패턴

import "sync"

func processAll(items []Item) {
    var wg sync.WaitGroup
    results := make([]Result, len(items))

    for i, item := range items {
        wg.Add(1)
        go func(i int, item Item) {   // passa i e item come parametri!
            defer wg.Done()
            results[i] = process(item) // scrivere indici diversi è safe
        }(i, item)
    }

    wg.Wait()           // blocca finché tutti Done() sono stati chiamati
    fmt.Println(results)
}

// ERRORE COMUNE: closure su variabile del loop (prima di Go 1.22)
// In Go 1.22+ il loop variable è scoped per iterazione -- no problema
for _, item := range items {
    go func() {
        process(item)    // Go 1.22+: safe. Go <1.22: BUG! usa go func(i Item) invece
    }()
}

인종 감지기: 인종 데이터 찾기

// Compila ed esegui con il race detector integrato
go run -race main.go
go test -race ./...

// Esempio di data race che il detector trova:
var counter int

func increment() {
    counter++  // DATA RACE! lettura + scrittura non atomica
}

// go run -race stamperà:
// WARNING: DATA RACE
// Write at 0x... by goroutine 6:
//   main.increment()
// Previous write at 0x... by goroutine 5:
//   main.increment()

// Fix con atomic o mutex:
import "sync/atomic"
var atomicCounter int64
atomic.AddInt64(&atomicCounter, 1)  // atomico, thread-safe

고루틴 누출: Go에서 가장 흔한 버그

수신 채널에서 멈추거나(아무도 전송하지 않음) 수신하지 못하는 고루틴 컨텍스트를 삭제하지 않는 것은 고루틴 누출입니다. 메모리와 CPU를 축적합니다. 항상 사용 context 긴 작업의 경우 각 채널에 발신자가 있는지 확인하세요. 및 수신기를 사용하거나 명시적인 정리와 함께 버퍼링된 채널을 사용합니다.

결론

Go의 동시성 모델은 백엔드 서비스에 가장 효과적인 모델 중 하나입니다. 초경량, 설계로 경쟁 조건을 방지하는 채널, select 다중화를 위해 전자 context 전파 취소의 경우. 대부분의 경쟁 Go 코드 명시적인 뮤텍스가 필요하지 않습니다.

다음 기사는 Python으로 넘어갑니다. asyncio.TaskGroup 그리고 구조화된 경쟁 Python 3.11+에서는 마침내 Go의 CSP에 필적하는 의미론적 안전성을 세상에 제공합니다. 비동기/대기.