Skip to content

✍️ 필사 모드: Go Complete Guide — Goroutine, Channel, Context, and Real-World Microservices (Season 2 Ep 3, 2025)

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

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 func iterators, 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

  1. Simplicity: the spec fits in your pocket
  2. Composition over Inheritance: interface plus embedding
  3. Explicit over Implicit: no magic
  4. Concurrency as First-class: Goroutine and Channel in the language
  5. Pragmatism: real-world over purity

1.3 Good At vs Bad At

Good atBad at
MicroservicesLow-level memory control
CLI toolsGPU computing
Network servicesReal-time systems
DevOps toolsGame engines
gRPC servicesMobile UI
Containers / orchestrationEmbedded 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 GOMAXPROCS OS 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() in select
  • 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

  1. Fan-out: many Goroutines consume one channel
  2. Fan-in: many channels merged into one
  3. Pipeline: stages of channels chained
  4. Worker Pool: N workers plus job channel
  5. 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

  1. Writing to a closed channel: panic. Use sync.Once to guarantee single close
  2. Send/receive on nil channel: blocks forever. Used as a disable trick in select
  3. 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

  1. Context is always the first argument: func DoWork(ctx context.Context, arg T) error
  2. Do not store in a struct: create fresh per request
  3. Never pass nil: use context.TODO() if unsure
  4. Value is for request metadata only: request ID, user ID, trace info. No business parameters
  5. 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, &notFoundErr) { /* ... */ }
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

  1. Deleting if err != nil and ignoring
  2. Logging and propagating at the same time: double logging
  3. Overusing panic(): only for unrecoverable cases
  4. Propagating without wrapping: no stack trace
  5. Capitalized or punctuated error messages: breaks Go convention

Part 6 — sync Package: Concurrency Primitives

6.1 Seven Core Types

TypeUse
sync.MutexMutual exclusion
sync.RWMutexMany readers / one writer
sync.WaitGroupWait for N Goroutines
sync.OnceRun exactly once
sync.CondCondition variable
sync.MapConcurrent map (specific patterns only)
sync.PoolObject 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

FrameworkCharacteristicsFit
net/httpStandard librarySimple services, strong since 1.22
chiMinimal, std-orientedMicroservice default
ginMature, popularStartups, fullstack
echoLike Gin, cleanAlternative
fiberExpress-like, fasthttpExtreme performance
humaOpenAPI-firstAPI 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

  1. Three axes of observability: metrics (Prometheus), logs (structured), traces (OpenTelemetry)
  2. Context propagation: pass ctx to every RPC
  3. Retry with backoff
  4. Circuit breaker: sony/gobreaker
  5. Timeout: explicit per call
  6. Health check: grpc-health-probe
  7. Graceful shutdown: drain on SIGTERM
  8. Rate limiting: golang.org/x/time/rate
  9. Request ID
  10. Deadline propagation

Part 10 — Go Ecosystem Top 20 for 2024-2025

CategoryPackages
Webchi, gin, echo, fiber
gRPCgrpc-go, buf, connect-go
DBdatabase/sql + pgx, sqlc, ent, gorm
Logginglog/slog (std, 1.21+), zerolog
Testingtestify, gomock, testcontainers-go
CLIcobra, viper, spf13/pflag
Observabilityprometheus/client_golang, opentelemetry-go
Concurrencygolang.org/x/sync/errgroup, semaphore
HTTP clientnet/http + retryablehttp
JSONencoding/json + json-iterator/go

10.1 Major Changes in Go 1.23-1.24

  • Go 1.23 (2024/08): range over func iterators stabilized, improved time.Timer
  • Go 1.24 (2025/02): Generic Type Alias, tool directive

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

ItemRustGo
Learning curve3-6 months2-4 weeks
Compile speedSlowVery fast
Runtime perfTop tierHigh
MemoryVery lowGC overhead
Concurrency safetyCompile-timeRuntime (-race)
Ecosystem maturityGrowing fastVery mature
DocsExcellentExcellent
Type systemPowerful, complexSimple

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

  1. Explain Goroutine vs OS thread
  2. Know behavior in three channel states (nil, open, closed)
  3. State five context propagation rules
  4. Know why errgroup exists
  5. Know how to detect race conditions
  6. Know when to pick sync.Mutex vs sync.RWMutex
  7. Know what %w means for error wrapping
  8. Know how to prevent Goroutine leaks
  9. Know the new net/http 1.22 router features
  10. Explain the gRPC plus buf plus connect stack
  11. Use slog structured logging
  12. Understand the harm of Generic overuse

Part 14 — 10 Go Anti-patterns

  1. Fire-and-forget Goroutine: no cancel, no wait -> leak
  2. Storing Context in a struct field
  3. panic() where an error return was correct
  4. Interface with 10+ methods: breaks the "small interface" rule
  5. Overusing empty interface (any, interface{}): abandons type safety
  6. Reading and writing shared variables in a goroutine without sync: race
  7. Wrapping errors with fmt.Errorf("%v"): not a wrap. Use %w
  8. Ignoring errors in defer: use a closure when the error matters
  9. Concurrent map access: runtime panic. Use sync.Map or sync.RWMutex
  10. nil interface comparison: var e error = (*MyError)(nil); e != nil is 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.

현재 단락 (1/379)

If Rust is "hard to learn but powerful", Go is "easy to learn and immediately production-ready".

작성 글자: 0원문 글자: 13,591작성 단락: 0/379