Split View: Go 완전 가이드 — Goroutine·Channel·Context·실전 마이크로서비스까지 (Season 2 Ep 3, 2025)
Go 완전 가이드 — Goroutine·Channel·Context·실전 마이크로서비스까지 (Season 2 Ep 3, 2025)
들어가며 — 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에 타입을 붙인 언어"라는 오해를 푸는 시간, 다음 글에서 이어진다.
Go Complete Guide — Goroutine, Channel, Context, and Real-World Microservices (Season 2 Ep 3, 2025)
Intro — Why Go Still Matters
If Rust is "hard to learn but powerful", Go is "easy to learn and immediately production-ready".
Go's position in 2024-2025:
- Kubernetes: the center of cloud native. Written in Go
- Docker: the origin of the container ecosystem
- Terraform: IaC standard
- Prometheus / Grafana: observability standard
- CockroachDB / InfluxDB / etcd: distributed DBs
- gRPC: Google's standard RPC, Go-first
- Cloudflare, Uber, Monzo, Square: primary microservice language
- Go 1.23 (Aug 2024):
range over funciterators, improved timers - Go 1.24 (Feb 2025): Generic Type Alias, improved tool directive
Go is the lingua franca of the Kubernetes world. For DevOps, SRE, and Cloud Native engineers it is not optional.
Part 1 — Go Philosophy: What Less is More Really Means
1.1 What Go Deliberately Omits
- Generics (missing until 1.18) — delayed on purpose
- Ternary operator — use
if-else - Exceptions (try/catch) — error as value
- Inheritance — composition only
- Macros — replaced by code generation
- Function overloading — use different names
Rob Pike: "Go is a language for people who feel Java has too much."
1.2 Five Design Principles
- Simplicity: the spec fits in your pocket
- Composition over Inheritance: interface plus embedding
- Explicit over Implicit: no magic
- Concurrency as First-class: Goroutine and Channel in the language
- Pragmatism: real-world over purity
1.3 Good At vs Bad At
| Good at | Bad at |
|---|---|
| Microservices | Low-level memory control |
| CLI tools | GPU computing |
| Network services | Real-time systems |
| DevOps tools | Game engines |
| gRPC services | Mobile UI |
| Containers / orchestration | Embedded firmware |
Part 2 — Goroutine: Go's Concurrency Model
2.1 What is a Goroutine
A lightweight thread. A single OS thread can schedule thousands of Goroutines.
go func() {
fmt.Println("hello from goroutine")
}()
- OS thread: ~2MB stack
- Goroutine: 2KB stack at start, grows dynamically
- Uses
GOMAXPROCSOS threads (defaults to CPU cores)
2.2 M:N Scheduler
- M: OS thread (Machine)
- N: Goroutine
- P: Processor (Goroutine queue manager)
The Go runtime multiplexes M Goroutines over N threads. Structurally similar to Tokio in Rust.
2.3 Goroutine Leak — the Most Common Production Bug
// Wrong: sender blocks forever if nobody receives
func leak() {
ch := make(chan int)
go func() {
ch <- 42 // no receiver, blocks forever
}()
// function returns, but goroutine is still alive
}
Fix patterns:
- Propagate cancellation with
context.Context - Include
<-ctx.Done()inselect - Buffered channel plus timeout
2.4 Three Goroutine Control Patterns
// 1. WaitGroup — wait for N tasks
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 — error propagation plus cancellation
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 — limit concurrency
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)
}()
}
Part 3 — Channel: Share Memory by Communicating
3.1 Channel Basics
ch := make(chan int) // Unbuffered
ch := make(chan int, 10) // Buffered
ch <- 42 // send
val := <-ch // receive
close(ch) // close
val, ok := <-ch // ok=false if closed and empty
for v := range ch { // iterate until closed
fmt.Println(v)
}
3.2 Unbuffered vs Buffered
- Unbuffered: sender and receiver synchronize (rendezvous)
- Buffered: sender does not block until the buffer fills
3.3 select — Multiplexing
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 is the real weapon of Go concurrency. Compose channels into complex patterns.
3.4 Five Channel Patterns
- Fan-out: many Goroutines consume one channel
- Fan-in: many channels merged into one
- Pipeline: stages of channels chained
- Worker Pool: N workers plus job channel
- Done Channel: cancellation signal (Context's ancestor)
// 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 Three Channel Traps
- Writing to a closed channel: panic. Use
sync.Onceto guarantee single close - Send/receive on nil channel: blocks forever. Used as a disable trick in
select - Over-synchronization: channels are not always better than mutexes
Part 4 — Context: The Bloodstream of the Go Ecosystem
4.1 What is Context
Standard interface for propagating cancellation, deadlines, and request-scoped values.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
4.2 Four Constructors
ctx := context.Background() // root
ctx := context.TODO() // decide later
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)
Always defer cancel() to prevent leaks.
4.3 Five Rules of Context Propagation
- Context is always the first argument:
func DoWork(ctx context.Context, arg T) error - Do not store in a struct: create fresh per request
- Never pass nil: use
context.TODO()if unsure - Value is for request metadata only: request ID, user ID, trace info. No business parameters
- Every blocking call must take ctx: DB, HTTP, gRPC — all of them
4.4 Context Value Anti-patterns
// Bad: business parameter via Value
ctx = context.WithValue(ctx, "userID", uid)
// Good: typed 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
}
Part 5 — Error Handling: Error as Value
5.1 Philosophy
result, err := doSomething()
if err != nil {
return nil, fmt.Errorf("doSomething failed: %w", err)
}
No exceptions. Errors are values. Handle explicitly or propagate.
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 (unwrappable)%v: format only (not unwrappable)
5.3 Sentinel Error vs Typed Error
// Sentinel — simple equality
var ErrNotFound = errors.New("not found")
// Typed — carries extra info
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string { /* ... */ }
5.4 Five Error Anti-patterns
- Deleting
if err != niland ignoring - Logging and propagating at the same time: double logging
- Overusing
panic(): only for unrecoverable cases - Propagating without wrapping: no stack trace
- Capitalized or punctuated error messages: breaks Go convention
Part 6 — sync Package: Concurrency Primitives
6.1 Seven Core Types
| Type | Use |
|---|---|
sync.Mutex | Mutual exclusion |
sync.RWMutex | Many readers / one writer |
sync.WaitGroup | Wait for N Goroutines |
sync.Once | Run exactly once |
sync.Cond | Condition variable |
sync.Map | Concurrent map (specific patterns only) |
sync.Pool | Object reuse (reduces GC pressure) |
6.2 atomic Package (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)
Faster than Mutex but only for simple operations.
6.3 Race Detection
go test -race ./...
go run -race main.go
Always verify -race passes before production. Must be in CI.
Part 7 — Generics (Go 1.18+)
7.1 Basic Syntax
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 Constraints
import "golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a, b T) T {
if a > b { return a }
return b
}
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 When Not to Use Generics
- Not a container
- Interface suffices (e.g.
io.Reader) - Compile time matters
Go team advice: "When in doubt, don't."
Part 8 — 2024-2025 Go Web Framework Comparison
| Framework | Characteristics | Fit |
|---|---|---|
| net/http | Standard library | Simple services, strong since 1.22 |
| chi | Minimal, std-oriented | Microservice default |
| gin | Mature, popular | Startups, fullstack |
| echo | Like Gin, clean | Alternative |
| fiber | Express-like, fasthttp | Extreme performance |
| huma | OpenAPI-first | API contract matters |
Go 1.22 enhanced net/http router:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUserHandler)
mux.HandleFunc("POST /users", createUserHandler)
http.ListenAndServe(":8080", mux)
Since 1.22, method and path variables are built-in, so a framework is often unnecessary.
8.1 Chi Example
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)
Part 9 — gRPC and Microservices in Practice
9.1 Why gRPC
- Protocol Buffers: type safety plus small payloads
- HTTP/2: multiplexing, header compression
- Streaming: server, client, bidirectional
- Code Generation: multi-language consistency
9.2 .proto File
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 (the 2025 standard)
- buf: lint, build, breaking-change detection for .proto
- ConnectRPC: gRPC plus REST plus gRPC-Web in one
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 Microservice Checklist
- Three axes of observability: metrics (Prometheus), logs (structured), traces (OpenTelemetry)
- Context propagation: pass ctx to every RPC
- Retry with backoff
- Circuit breaker:
sony/gobreaker - Timeout: explicit per call
- Health check: grpc-health-probe
- Graceful shutdown: drain on SIGTERM
- Rate limiting:
golang.org/x/time/rate - Request ID
- Deadline propagation
Part 10 — Go Ecosystem Top 20 for 2024-2025
| Category | Packages |
|---|---|
| Web | chi, gin, echo, fiber |
| gRPC | grpc-go, buf, connect-go |
| DB | database/sql + pgx, sqlc, ent, gorm |
| Logging | log/slog (std, 1.21+), zerolog |
| Testing | testify, gomock, testcontainers-go |
| CLI | cobra, viper, spf13/pflag |
| Observability | prometheus/client_golang, opentelemetry-go |
| Concurrency | golang.org/x/sync/errgroup, semaphore |
| HTTP client | net/http + retryablehttp |
| JSON | encoding/json + json-iterator/go |
10.1 Major Changes in Go 1.23-1.24
- Go 1.23 (2024/08):
rangeover func iterators stabilized, improvedtime.Timer - Go 1.24 (2025/02): Generic Type Alias,
tooldirective
Part 11 — Rust vs Go Decision Guide
11.1 Decision Tree
Q1. Are memory and latency at the limit?
Yes -> Rust
No -> Q2
Q2. Large team with fast onboarding?
Yes -> Go
No -> Q3
Q3. Systems programming (kernel, DB, runtime)?
Yes -> Rust
No -> Q4
Q4. Kubernetes / DevOps ecosystem?
Yes -> Go
No -> Q5
Q5. GC pauses unacceptable?
Yes -> Rust
No -> Go (most of the time)
11.2 Honest Comparison
| Item | Rust | Go |
|---|---|---|
| Learning curve | 3-6 months | 2-4 weeks |
| Compile speed | Slow | Very fast |
| Runtime perf | Top tier | High |
| Memory | Very low | GC overhead |
| Concurrency safety | Compile-time | Runtime (-race) |
| Ecosystem maturity | Growing fast | Very mature |
| Docs | Excellent | Excellent |
| Type system | Powerful, complex | Simple |
Everyday calls:
- Backend API server -> Go (10 minutes)
- DB, runtime, proxy -> Rust (extreme performance)
- Either works -> pick the team's language
Part 12 — Six-Month Go Roadmap
Month 1: Fundamentals
- Finish Tour of Go
- Read Effective Go
- Build one CLI tool
Month 2: Concurrency
- Concurrency in Go (Katherine Cox-Buday)
- Write a Worker Pool yourself
- Practice race detection with
-race
Month 3: Deep into the Standard Library
- Read net/http sources
- Internalize context patterns
- Real errgroup plus semaphore
Month 4: Real Server
- API with chi plus pgx plus PostgreSQL
- Three axes of observability (slog plus prometheus plus otel)
- Integration tests with testcontainers
Month 5: gRPC and Microservices
- Protocol Buffers
- grpc-go or connect-go
- Three-service comms plus observability
Month 6: OSS Contribution
- Close one issue in Kubernetes, Istio, or Prometheus
Part 13 — 12 Go Checkpoints
- Explain Goroutine vs OS thread
- Know behavior in three channel states (nil, open, closed)
- State five context propagation rules
- Know why errgroup exists
- Know how to detect race conditions
- Know when to pick
sync.Mutexvssync.RWMutex - Know what
%wmeans for error wrapping - Know how to prevent Goroutine leaks
- Know the new net/http 1.22 router features
- Explain the gRPC plus buf plus connect stack
- Use slog structured logging
- Understand the harm of Generic overuse
Part 14 — 10 Go Anti-patterns
- Fire-and-forget Goroutine: no cancel, no wait -> leak
- Storing Context in a struct field
panic()where an error return was correct- Interface with 10+ methods: breaks the "small interface" rule
- Overusing empty interface (
any,interface{}): abandons type safety - Reading and writing shared variables in a goroutine without sync: race
- Wrapping errors with
fmt.Errorf("%v"): not a wrap. Use%w - Ignoring errors in defer: use a closure when the error matters
- Concurrent map access: runtime panic. Use
sync.Maporsync.RWMutex - nil interface comparison:
var e error = (*MyError)(nil); e != nilis true
Closing — Go is Strong Because It's Boring
Go is not flashy. Syntax takes ten minutes. Almost no modern bells.
But that "boredom" is team productivity. A new engineer ships production code in two weeks. Ten-year-old code still reads. That is why Kubernetes can live at 5M lines.
If Rust's philosophy is "if it's wrong it won't compile", Go's is "when many people write it, everyone can read it".
In 2025, a senior engineer's toolbox should have both Rust and Go. Pull the right one for the job.
Next Up — TypeScript Complete Guide
Season 2 Ep 4 is TypeScript. Next:
- New type operators in TypeScript 5.x
- Generics and Conditional Type
- Template Literal Types in practice
- Type-safe fullstack with Zod plus tRPC plus TanStack
- 2024-2025 TS ecosystem (Turborepo, Bun, Deno)
- Writing TypeScript with AI
See you next time.