- Authors

- Name
- Youngju Kim
- @fjvbn20031
들어가며 — 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가지
- Simplicity: 언어 스펙을 주머니에 들고 다닐 수 있을 만큼 작게
- Composition over Inheritance: interface + embedding으로 충분
- Explicit over Implicit: magic 없음. 모든 동작이 코드에 드러나야
- Concurrency as First-class: Goroutine·Channel은 언어 차원
- 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 — 에러 전파 + 취소
import "golang.org/x/sync/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 — 동시 실행 수 제한
import "golang.org/x/sync/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가지
- Fan-out: 하나의 Channel을 여러 Goroutine이 소비
- Fan-in: 여러 Channel을 하나로 합침
- Pipeline: 단계별 Channel 체인
- Worker Pool: N개 Worker + Job Channel
- 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가지
- 닫힌 Channel에 쓰기: panic.
sync.Once로 한 번만 close 보장 - nil Channel 송수신: 영원히 블록. select에서 비활성화 트릭으로 사용
- 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가지
- Context는 항상 첫 번째 인자:
func DoWork(ctx context.Context, arg T) error - Struct에 저장하지 말 것: 요청마다 새로 생성
- nil 전달 금지: 모호하면
context.TODO() - Value는 요청 메타데이터용: request ID, user ID, 추적 정보. 비즈니스 파라미터는 ❌
- 모든 블로킹 호출이 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, ¬FoundErr) { /* ... */ }
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가지
if err != nil지우고 무시:_ = err는 지옥으로 가는 길- 에러 로그 + 전파 동시: 중복 로깅. 한 곳에서만 처리
panic()남발: 복구 불가능한 경우에만- wrap 없이 전파: 스택 추적 불가
- 에러 메시지 대문자·구두점: 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+)
import "sync/atomic"
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
import "golang.org/x/exp/constraints"
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 예제 (권장)
import "github.com/go-chi/chi/v5"
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을 한 번에 지원
import "connectrpc.com/connect"
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 마이크로서비스 체크리스트
- 관측성 3축: metrics (Prometheus), logs (structured), traces (OpenTelemetry)
- Context 전파: 모든 RPC에 ctx 전달
- Retry with backoff:
google.golang.org/grpc/retry - Circuit Breaker:
sony/gobreaker - Timeout: 각 호출마다 명시
- Health Check: grpc-health-probe
- Graceful Shutdown: SIGTERM 받으면 기존 요청 완료 후 종료
- Rate Limit:
golang.org/x/time/rate - Request ID: 요청 추적
- 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):
rangeover func 이터레이터 안정화, 개선된time.Timer - Go 1.24 (2025/02): Generic Type Alias,
tooldirective (도구 의존성 관리)
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
- Goroutine과 OS 스레드 차이를 설명할 수 있다
- Channel 3가지 상태(nil, open, closed)에서의 동작을 안다
- Context 전파 원칙 5개를 말할 수 있다
- errgroup 사용 이유를 안다
- Race Condition 탐지 방법을 안다 (
-race) - sync.Mutex vs sync.RWMutex 선택 기준을 안다
- Error Wrapping (
%w) 의미를 안다 - Goroutine 누수를 어떻게 예방하는지 안다
- net/http 1.22 라우터의 새 기능을 안다
- gRPC + buf + connect 스택을 설명할 수 있다
- slog 구조화 로깅을 쓸 줄 안다
- Generic 남용의 해악을 이해한다
14부 — Go 안티패턴 10
- Goroutine을 fire-and-forget: 취소·완료 대기 없음 → 누수
- Context를 struct 필드로 저장: 철학 위반
panic()대신 에러 반환을 해야 할 상황에panic- interface에 메서드 10개 이상: "Go interface는 작게" 원칙 위반
- 빈 interface(
any,interface{}) 남용: 타입 안전성 포기 - Goroutine 내부에서 공유 변수 읽기·쓰기 without 동기화: race
- 에러
fmt.Errorf("%v")로 감싸기: wrap 안 됨.%w써야 - defer 안에서 에러 무시:
defer f.Close()→ 에러 처리 필요하면 익명 func - map 동시 접근: 런타임 panic.
sync.Map또는sync.RWMutex - 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에 타입을 붙인 언어"라는 오해를 푸는 시간, 다음 글에서 이어진다.