Skip to content

필사 모드: Go 완전 가이드 — Goroutine·Channel·Context·실전 마이크로서비스까지 (Season 2 Ep 3, 2025)

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — Go는 왜 여전히 쓰이는가

Rust가 "배우기 어렵지만 강력한" 쪽이라면, Go는 "배우기 쉽지만 프로덕션에 즉시 투입 가능한" 쪽이다.

**2024~2025 Go의 현재 위치:**

- **Kubernetes**: 클라우드 네이티브의 중심. Go로 작성됨

- **Docker**: 컨테이너 생태계의 원조. Go

- **Terraform**: IaC의 표준. Go

- **Prometheus / Grafana**: 관측성 표준. Go

- **CockroachDB / InfluxDB / etcd**: 분산 DB. Go

- **gRPC**: Google 표준 RPC. Go 퍼스트

- **Cloudflare, Uber, Monzo, Square**: 마이크로서비스 주력 언어

- **Go 1.23** (2024년 8월): `range over func` 이터레이터, 개선된 timer

- **Go 1.24** (2025년 2월): Generic Type Alias, 향상된 tool directive

Go는 "쿠버네티스 세상의 공용어"다. DevOps·SRE·Cloud Native 엔지니어에게는 선택이 아니라 필수.

1부 — Go의 철학: Less is More의 진짜 의미

1.1 Go가 의도적으로 제외한 것들

Go는 명시적으로 **언어에서 제외한 것**이 많다:

- Generic (1.18까지 없었음) → 의도적으로 늦게 도입

- 삼항 연산자 → `if-else`로 통일

- 예외(try/catch) → Error as value

- 상속 → Composition only

- Macro → 코드 생성 도구로 대체

- 함수 오버로딩 → 이름 다르게

Rob Pike (Go 창시자) 왈: **"Go는 Java가 너무 많다고 느끼는 사람들을 위한 언어다."**

1.2 Go의 설계 원칙 5가지

1. **Simplicity**: 언어 스펙을 주머니에 들고 다닐 수 있을 만큼 작게

2. **Composition over Inheritance**: interface + embedding으로 충분

3. **Explicit over Implicit**: magic 없음. 모든 동작이 코드에 드러나야

4. **Concurrency as First-class**: Goroutine·Channel은 언어 차원

5. **Pragmatism**: 순수함보다 현장성

1.3 Go가 잘하는 것 vs 못하는 것

| 잘함 | 못함 |

|------|------|

| 마이크로서비스 | 저수준 메모리 제어 |

| CLI 도구 | GPU 컴퓨팅 |

| 네트워크 프로그램 | 실시간 시스템 |

| DevOps 도구 | 게임 엔진 |

| gRPC 서비스 | 모바일 UI |

| 컨테이너/오케스트레이션 | 임베디드 펌웨어 |

2부 — Goroutine: Go의 동시성 모델

2.1 Goroutine이란

**경량 스레드**. OS 스레드 1개가 수천 개의 Goroutine을 스케줄링.

go func() {

fmt.Println("hello from goroutine")

}()

- OS 스레드: ~2MB 스택

- Goroutine: 시작 시 **2KB 스택**, 필요 시 동적 확장

- GOMAXPROCS (기본 CPU 코어 수)만큼 OS 스레드 사용

2.2 M:N 스케줄러

- **M**: OS 스레드 (Machine)

- **N**: Goroutine

- **P**: 프로세서 (Goroutine 큐 관리)

Go 런타임이 M개 Goroutine을 N개 스레드 위에 멀티플렉싱. Rust async의 Tokio와 구조적으로 유사.

2.3 Goroutine 누수 — 가장 흔한 프로덕션 버그

// 잘못된 코드: receiver가 없으면 sender는 영원히 대기

func leak() {

ch := make(chan int)

go func() {

ch <- 42 // 받는 사람 없음 → 영원히 블록

}()

// 함수 종료, 하지만 goroutine은 살아있음

}

**해결 패턴:**

- `context.Context`로 취소 전파

- `select`에 `<-ctx.Done()` 포함

- Buffered channel + timeout

2.4 Goroutine 제어 패턴 3가지

// 1. WaitGroup — N개 작업 완료 대기

var wg sync.WaitGroup

for i := 0; i < 5; i++ {

wg.Add(1)

go func(id int) {

defer wg.Done()

// work

}(i)

}

wg.Wait()

// 2. errgroup — 에러 전파 + 취소

g, ctx := errgroup.WithContext(ctx)

for _, url := range urls {

url := url

g.Go(func() error {

return fetch(ctx, url)

})

}

if err := g.Wait(); err != nil { /* ... */ }

// 3. Semaphore — 동시 실행 수 제한

sem := semaphore.NewWeighted(10)

for _, task := range tasks {

task := task

sem.Acquire(ctx, 1)

go func() {

defer sem.Release(1)

do(task)

}()

}

3부 — Channel: "Don't communicate by sharing memory. Share memory by communicating."

3.1 Channel 기본

ch := make(chan int) // Unbuffered

ch := make(chan int, 10) // Buffered

ch <- 42 // 송신

val := <-ch // 수신

close(ch) // 닫기

val, ok := <-ch // ok=false면 닫혔고 비었음

for v := range ch { // 닫힐 때까지 반복

fmt.Println(v)

}

3.2 Unbuffered vs Buffered

- **Unbuffered**: 송신자와 수신자가 **동기화**. Sender는 Receiver가 준비될 때까지 대기 (Rendezvous)

- **Buffered**: 버퍼가 차기 전까지 Sender는 블록되지 않음

3.3 select — 멀티플렉싱

select {

case msg := <-ch1:

fmt.Println("from ch1", msg)

case msg := <-ch2:

fmt.Println("from ch2", msg)

case <-time.After(1 * time.Second):

fmt.Println("timeout")

case <-ctx.Done():

fmt.Println("canceled")

}

`select`가 Go 동시성의 진짜 무기. Channel을 조합해 복잡한 패턴 구축.

3.4 주요 Channel 패턴 5가지

1. **Fan-out**: 하나의 Channel을 여러 Goroutine이 소비

2. **Fan-in**: 여러 Channel을 하나로 합침

3. **Pipeline**: 단계별 Channel 체인

4. **Worker Pool**: N개 Worker + Job Channel

5. **Done Channel**: 취소 신호 전파 (Context의 전신)

// Worker Pool

jobs := make(chan Job, 100)

results := make(chan Result, 100)

for w := 1; w <= 10; w++ {

go worker(jobs, results)

}

for _, j := range allJobs {

jobs <- j

}

close(jobs)

for a := 1; a <= len(allJobs); a++ {

<-results

}

3.5 Channel 함정 3가지

1. **닫힌 Channel에 쓰기**: panic. `sync.Once`로 한 번만 close 보장

2. **nil Channel 송수신**: 영원히 블록. select에서 비활성화 트릭으로 사용

3. **Over-synchronization**: Channel이 Mutex보다 항상 좋은 건 아님

4부 — Context: Go 생태계의 혈류

4.1 Context란

**요청 범위의 취소·데드라인·값을 전파하는 표준 인터페이스**.

type Context interface {

Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key any) any

}

4.2 4가지 기본 생성 함수

ctx := context.Background() // 루트

ctx := context.TODO() // 나중에 정하겠다

ctx, cancel := context.WithCancel(parent)

ctx, cancel := context.WithTimeout(parent, 5*time.Second)

ctx, cancel := context.WithDeadline(parent, time.Now().Add(1*time.Minute))

ctx := context.WithValue(parent, key, val) // 값 전파

**반드시 `defer cancel()`** — 리소스 누수 방지.

4.3 Context 전파 원칙 5가지

1. **Context는 항상 첫 번째 인자**: `func DoWork(ctx context.Context, arg T) error`

2. **Struct에 저장하지 말 것**: 요청마다 새로 생성

3. **nil 전달 금지**: 모호하면 `context.TODO()`

4. **Value는 요청 메타데이터용**: request ID, user ID, 추적 정보. 비즈니스 파라미터는 ❌

5. **모든 블로킹 호출이 ctx를 받아야**: DB, HTTP, gRPC 전부

4.4 Context Value의 안티패턴

// 나쁨: 비즈니스 파라미터를 Value로 전달

ctx = context.WithValue(ctx, "userID", uid)

// 좋음: 타입 안전한 key 정의

type ctxKey string

const userIDKey ctxKey = "userID"

ctx = context.WithValue(ctx, userIDKey, uid)

func GetUserID(ctx context.Context) (string, bool) {

uid, ok := ctx.Value(userIDKey).(string)

return uid, ok

}

5부 — Error Handling: Error as Value

5.1 Go의 에러 철학

result, err := doSomething()

if err != nil {

return nil, fmt.Errorf("doSomething failed: %w", err)

}

**예외 없음. 에러는 값.** 명시적으로 처리하거나 전파.

5.2 Error Wrapping (Go 1.13+)

if err != nil {

return fmt.Errorf("failed to load user %d: %w", id, err)

}

// 체크

var notFoundErr *NotFoundError

if errors.As(err, &notFoundErr) { /* ... */ }

if errors.Is(err, sql.ErrNoRows) { /* ... */ }

- `%w`: wrap (언래핑 가능)

- `%v`: 문자열 포매팅 (언래핑 불가)

5.3 Sentinel Error vs Typed Error

// Sentinel — 간단한 비교

var ErrNotFound = errors.New("not found")

// Typed — 추가 정보 담기

type NotFoundError struct {

Resource string

ID string

}

func (e *NotFoundError) Error() string { /* ... */ }

5.4 에러 처리 안티패턴 5가지

1. **`if err != nil` 지우고 무시**: `_ = err`는 지옥으로 가는 길

2. **에러 로그 + 전파 동시**: 중복 로깅. 한 곳에서만 처리

3. **`panic()` 남발**: 복구 불가능한 경우에만

4. **wrap 없이 전파**: 스택 추적 불가

5. **에러 메시지 대문자·구두점**: Go 컨벤션 위반 (`failed to X`, no `.` at end)

6부 — sync 패키지: 동시성 프리미티브

6.1 핵심 타입 7가지

| 타입 | 용도 |

|-----|-----|

| `sync.Mutex` | 상호 배제 |

| `sync.RWMutex` | 다중 읽기 / 단일 쓰기 |

| `sync.WaitGroup` | N개 Goroutine 완료 대기 |

| `sync.Once` | 딱 한 번 실행 |

| `sync.Cond` | 조건 변수 |

| `sync.Map` | 동시성 안전 맵 (특정 패턴에만 유리) |

| `sync.Pool` | 객체 재사용 (GC 압박 감소) |

6.2 atomic 패키지 (Go 1.19+)

var counter atomic.Int64

counter.Add(1)

v := counter.Load()

counter.Store(100)

var ready atomic.Bool

ready.Store(true)

Mutex보다 빠르지만 단순 연산에만 사용.

6.3 Race Condition 탐지

go test -race ./...

go run -race main.go

**프로덕션 배포 전 반드시 `-race` 통과 확인.** CI에 포함 필수.

7부 — Generic (Go 1.18+): 늦게 도착한 구원

7.1 기본 문법

func Map[T, U any](s []T, f func(T) U) []U {

result := make([]U, len(s))

for i, v := range s {

result[i] = f(v)

}

return result

}

doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })

7.2 Type Constraint

func Max[T constraints.Ordered](a, b T) T {

if a > b { return a }

return b

}

// 커스텀 Constraint

type Number interface {

int | int64 | float64

}

func Sum[T Number](s []T) T {

var total T

for _, v := range s { total += v }

return total

}

7.3 Generic을 쓰지 말아야 할 때

- **Container가 아닐 때**: 대부분의 비즈니스 로직엔 불필요

- **interface가 충분할 때**: io.Reader 같은 것

- **컴파일 시간이 더 중요할 때**: Generic은 컴파일 시간 증가

**Go 팀 공식 조언**: "의심되면 쓰지 마라."

8부 — 2024~2025 Go Web Framework 비교

| 프레임워크 | 특징 | 적합 |

|-----------|-----|-----|

| **net/http** | 표준 라이브러리 | 단순 서비스, Go 1.22+ 강력 |

| **chi** | 미니멀, 표준 중심 | 마이크로서비스 기본값 |

| **gin** | 성숙·인기 | 스타트업·풀스택 |

| **echo** | Gin과 유사, 깔끔 | 대안 |

| **fiber** | Express-like, fasthttp | 극한 퍼포먼스 |

| **huma** | OpenAPI 퍼스트 | API 계약 중요 |

**Go 1.22의 강력한 net/http 라우터:**

mux := http.NewServeMux()

mux.HandleFunc("GET /users/{id}", getUserHandler)

mux.HandleFunc("POST /users", createUserHandler)

http.ListenAndServe(":8080", mux)

1.22부터 메서드·경로 변수 기본 지원 → 많은 경우 프레임워크 불필요.

8.1 Chi 예제 (권장)

r := chi.NewRouter()

r.Use(middleware.Logger)

r.Use(middleware.Recoverer)

r.Use(middleware.Timeout(5 * time.Second))

r.Route("/api/v1", func(r chi.Router) {

r.Get("/users", listUsers)

r.Post("/users", createUser)

r.Get("/users/{id}", getUser)

})

http.ListenAndServe(":8080", r)

9부 — gRPC와 마이크로서비스 실전

9.1 왜 gRPC인가

- **Protocol Buffers**: 타입 안전 + 작은 페이로드

- **HTTP/2**: 멀티플렉싱, 헤더 압축

- **Streaming**: 서버·클라이언트·양방향

- **Code Generation**: 여러 언어 간 일관성

9.2 .proto 파일

syntax = "proto3";

package user.v1;

option go_package = "example.com/gen/user/v1;userv1";

service UserService {

rpc GetUser(GetUserRequest) returns (User);

rpc ListUsers(ListUsersRequest) returns (stream User);

}

message User {

string id = 1;

string name = 2;

int32 age = 3;

}

message GetUserRequest { string id = 1; }

message ListUsersRequest { int32 page_size = 1; }

9.3 buf + ConnectRPC (2025 표준)

- **buf**: .proto 린트·빌드·브레이킹 체인지 감지

- **ConnectRPC**: gRPC + REST + gRPC-Web을 한 번에 지원

type server struct{}

func (s *server) GetUser(

ctx context.Context,

req *connect.Request[userv1.GetUserRequest],

) (*connect.Response[userv1.User], error) {

return connect.NewResponse(&userv1.User{Id: req.Msg.Id}), nil

}

9.4 마이크로서비스 체크리스트

1. **관측성 3축**: metrics (Prometheus), logs (structured), traces (OpenTelemetry)

2. **Context 전파**: 모든 RPC에 ctx 전달

3. **Retry with backoff**: `google.golang.org/grpc/retry`

4. **Circuit Breaker**: `sony/gobreaker`

5. **Timeout**: 각 호출마다 명시

6. **Health Check**: grpc-health-probe

7. **Graceful Shutdown**: SIGTERM 받으면 기존 요청 완료 후 종료

8. **Rate Limit**: `golang.org/x/time/rate`

9. **Request ID**: 요청 추적

10. **Deadline Propagation**: 상위 Deadline을 하위 호출에 전달

10부 — Go 생태계 2024~2025 핵심 20

| 카테고리 | 패키지 |

|---------|--------|

| **Web** | `chi`, `gin`, `echo`, `fiber` |

| **gRPC** | `grpc-go`, `buf`, `connect-go` |

| **DB** | `database/sql` + `pgx`, `sqlc` (compile-time queries), `ent`, `gorm` |

| **로깅** | `log/slog` (표준, 1.21+), `zerolog` |

| **테스트** | `testify`, `gomock`, `testcontainers-go` |

| **CLI** | `cobra`, `viper`, `spf13/pflag` |

| **관측성** | `prometheus/client_golang`, `opentelemetry-go` |

| **동시성** | `golang.org/x/sync/errgroup`, `semaphore` |

| **HTTP 클라이언트** | `net/http` + `retryablehttp` |

| **JSON** | `encoding/json` + `json-iterator/go` (속도) |

10.1 Go 1.23~1.24 주요 변화

- **Go 1.23 (2024/08)**: `range` over func 이터레이터 안정화, 개선된 `time.Timer`

- **Go 1.24 (2025/02)**: Generic Type Alias, `tool` directive (도구 의존성 관리)

11부 — Rust vs Go 선택 가이드

11.1 결정 트리

Q1. 메모리·레이턴시가 극한인가?

Yes → Rust

No → Q2

Q2. 팀 규모가 크고 빠른 온보딩이 중요한가?

Yes → Go

No → Q3

Q3. 시스템 프로그래밍(커널·DB·런타임)인가?

Yes → Rust

No → Q4

Q4. Kubernetes/DevOps 생태계인가?

Yes → Go

No → Q5

Q5. GC 일시정지가 허용 불가한가?

Yes → Rust

No → Go (대부분의 경우)

11.2 냉정한 비교표

| 항목 | Rust | Go |

|-----|------|-----|

| 학습 곡선 | 3~6개월 | 2~4주 |

| 컴파일 속도 | 느림 | 매우 빠름 |

| 런타임 성능 | 최상위 | 높음 |

| 메모리 사용 | 매우 적음 | GC 오버헤드 |

| 동시성 안전성 | 컴파일 타임 검증 | 런타임 (`-race`) |

| 생태계 성숙도 | 빠르게 성장 | 매우 성숙 |

| 도큐먼트 | 훌륭 | 훌륭 |

| 타입 시스템 | 강력·복잡 | 단순 |

**일상 의사결정**:

- 백엔드 API 서버 → Go (10분 만에 만들 수 있음)

- DB·런타임·프록시 → Rust (극한 성능)

- 둘 다 되는 상황 → 팀의 언어

12부 — Go 마스터 로드맵 6개월

Month 1: 기본기

- Tour of Go 완주

- Effective Go 읽기

- CLI 도구 1개 만들기

Month 2: 동시성

- Concurrency in Go (Katherine Cox-Buday)

- Worker Pool 예제 직접 작성

- `-race`로 race condition 탐지 연습

Month 3: 표준 라이브러리 깊이

- net/http 소스 읽기

- context 패턴 체화

- errgroup + semaphore 실전

Month 4: 실전 서버

- chi + pgx + PostgreSQL로 API 만들기

- 관측성 3축 (slog + prometheus + otel)

- testcontainers로 통합 테스트

Month 5: gRPC·마이크로서비스

- Protocol Buffers

- grpc-go 또는 connect-go

- 3개 서비스 간 통신 + 관측성

Month 6: 오픈소스 기여

- Kubernetes·Istio·Prometheus 이슈 1개 해결

- 또는 crates.io에 버전 올리기

13부 — Go 체크리스트 12

1. **Goroutine과 OS 스레드 차이**를 설명할 수 있다

2. **Channel 3가지 상태**(nil, open, closed)에서의 동작을 안다

3. **Context 전파 원칙 5개**를 말할 수 있다

4. **errgroup 사용 이유**를 안다

5. **Race Condition 탐지 방법**을 안다 (`-race`)

6. **sync.Mutex vs sync.RWMutex** 선택 기준을 안다

7. **Error Wrapping (`%w`) 의미**를 안다

8. **Goroutine 누수를 어떻게 예방**하는지 안다

9. **net/http 1.22 라우터**의 새 기능을 안다

10. **gRPC + buf + connect** 스택을 설명할 수 있다

11. **slog 구조화 로깅**을 쓸 줄 안다

12. **Generic 남용의 해악**을 이해한다

14부 — Go 안티패턴 10

1. **Goroutine을 fire-and-forget**: 취소·완료 대기 없음 → 누수

2. **Context를 struct 필드로 저장**: 철학 위반

3. **`panic()` 대신 에러 반환을 해야 할 상황에 `panic`**

4. **interface에 메서드 10개 이상**: "Go interface는 작게" 원칙 위반

5. **빈 interface(`any`, `interface{}`) 남용**: 타입 안전성 포기

6. **Goroutine 내부에서 공유 변수 읽기·쓰기 without 동기화**: race

7. **에러 `fmt.Errorf("%v")`로 감싸기**: wrap 안 됨. `%w` 써야

8. **defer 안에서 에러 무시**: `defer f.Close()` → 에러 처리 필요하면 익명 func

9. **map 동시 접근**: 런타임 panic. `sync.Map` 또는 `sync.RWMutex`

10. **nil interface 비교 실수**: `var e error = (*MyError)(nil); e != nil`은 true

마치며 — Go는 "지루해서 강하다"

Go는 화려하지 않다. 문법은 10분이면 다 배운다. 최신 트렌드도 거의 없다.

하지만 **그 "지루함"이 곧 팀의 생산성**이다. 새 엔지니어가 2주 만에 프로덕션 코드를 작성하고, 10년 된 코드가 여전히 읽힌다. Kubernetes가 500만 줄로도 버티는 이유.

Rust가 "틀리면 컴파일 안 됨"의 철학이라면, Go는 **"많이 쓰면 읽을 수 있다"의 철학**이다.

2025년, 시니어 엔지니어의 도구함에는 **Rust와 Go가 둘 다 있어야 한다**. 상황에 따라 꺼내 쓰는 것.

다음 글 예고 — "TypeScript 완전 가이드: 타입 레벨 프로그래밍·Generics·제네레이티브 AI 생산성"

Season 2 Ep 4는 현대 웹의 사실상 표준, **TypeScript**. 다음 글은:

- TypeScript 5.x의 새로운 타입 연산자들

- Generics와 Conditional Type

- Template Literal Type의 실전 활용

- Zod + tRPC + TanStack으로 타입 안전한 풀스택

- 2024~2025 TS 생태계 (Turborepo, Bun, Deno)

- AI와 함께 TypeScript 쓰는 법

"JavaScript에 타입을 붙인 언어"라는 오해를 푸는 시간, 다음 글에서 이어진다.

현재 단락 (1/383)

Rust가 "배우기 어렵지만 강력한" 쪽이라면, Go는 "배우기 쉽지만 프로덕션에 즉시 투입 가능한" 쪽이다.

작성 글자: 0원문 글자: 10,981작성 단락: 0/383