왜 Go를 쓰는가
Go를 최신 문법 기능보다 배포 단순성, 빠른 컴파일, 표준 도구 체인, 동시성 모델 관점에서 어떻게 읽어야 하는지 정리합니다.
package main
import "fmt"
func main() {
fmt.Println("hello, go")
}Category
Preparing references and filters for this topic. 이 주제의 레퍼런스와 필터를 준비하고 있습니다.
Go를 최신 문법 기능보다 배포 단순성, 빠른 컴파일, 표준 도구 체인, 동시성 모델 관점에서 어떻게 읽어야 하는지 정리합니다.
package main
import "fmt"
func main() {
fmt.Println("hello, go")
}`go mod init`부터 패키지 경계, `cmd/`와 `internal/` 같은 디렉터리 감각까지 Go 프로젝트를 어떻게 시작하면 좋은지 정리합니다.
mkdir myapp
cd myapp
go mod init example.com/myappGo에서 package가 디렉터리와 어떻게 연결되고, import 경로와 대문자 공개 규칙을 어떻게 읽어야 하는지 정리합니다.
package user
import "time"
type Profile struct {
ID string
createdAt time.Time
}`go.mod`, `go.work`, `replace`가 각각 어떤 의존성 경계와 로컬 개발 흐름을 담당하는지 정리합니다.
`var`, `:=`, `const`, zero value를 어떤 형태로 쓰고, 선언 위치와 타입 추론을 어떻게 읽어야 하는지 정리합니다.
var name string
var count = 3
ready, retries := true, 3
const (
maxRetries = 5
kib = 1 << (10 * iota)
)Go의 `if`, `for`, `range`, `switch`가 어떤 형태를 갖고, 반복과 분기를 어떤 기준으로 고르는지 정리합니다.
if err != nil {
return err
}
for i := 0; i < len(items); i++ {
fmt.Println(items[i])
}
for count < limit {
count++
}
for _, item := range items {
fmt.Println(item)
}
for i := range 3 { // Go 1.22+
fmt.Println(i)
}
for {
break
}
switch status {
case http.StatusOK:
return nil
default:
return fmt.Errorf("bad status: %d", status)
}
switch {
case score >= 90:
return "A"
}Go 함수 시그니처의 주요 형태, 다중 반환으로 값과 error를 함께 돌려주는 패턴, `defer`의 실행 시점을 정리합니다.
func ping() {}
func add(a int, b int) int {
return a + b
}
func readUser(id string) (User, error) {
// ...
}
func writeReport(path string, body []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if closeErr := f.Close(); err == nil {
err = closeErr
}
}()
_, err = f.Write(body)
return err
}Go에서 데이터를 `struct`로 묶고, 동작을 method로 붙이고, 계약은 암시적 interface로 읽는 기본 흐름을 정리합니다.
type User struct {
Name string
Age int
}
func (u User) IsAdult() bool {
return u.Age >= 20
}
type Renamer interface {
Rename(string)
}
func (u *User) Rename(name string) {
u.Name = name
}`&`, `*`, value copy, pointer receiver를 어떻게 읽고, 값 전달과 포인터 전달을 어떤 기준으로 고르는지 정리합니다.
count := 3
ptr := &count
*ptr = 4
fmt.Println(count) // 4Go에서 slice와 map을 어떻게 만들고, nil과 empty의 차이를 어떻게 읽으며, `make`가 언제 필요한지 정리합니다.
names := []string{"Mina", "Jin"}
names = append(names, "Noah")
scores := make(map[string]int)
scores["Mina"] = 10
if score, ok := scores["Jin"]; ok {
_ = score
}
buf := make([]byte, 0, 1024)
seen := make(map[string]bool)Go 제네릭의 타입 매개변수, constraint, zero value 반환, generic type을 언제 쓰고 언제 피해야 하는지 정리합니다.
func First[T any](items []T) (T, bool) {
if len(items) == 0 {
var zero T
return zero, false
}
return items[0], true
}
func Zero[T any]() T {
var zero T
return zero
}Go에서 예외 대신 `error`를 어떻게 반환하는지, `%w`와 `errors.Is`/`errors.As`로 에러 문맥을 어떻게 보존하는지 정리합니다.
user, err := repo.Find(id)
if errors.Is(err, os.ErrNotExist) {
return ErrMissingUser
}
if err != nil {
return fmt.Errorf("load user %s: %w", id, err)
}`go test`의 기본 흐름, `testing.T` 사용법, Go에서 자주 쓰는 table-driven test 패턴을 정리합니다.
go test ./...
go test -run '^TestParsePort$' ./internal/config
go test -count=1 ./...Go에서 `panic`, `recover`, `defer`가 어떤 실패 경계에서 쓰이는지, 일반 error 반환과 어떻게 구분해야 하는지 정리합니다.
`testing.F`, seed corpus, `go test -fuzz`를 이용해 unit test가 놓치기 쉬운 입력 조합을 찾는 fuzzing 흐름을 정리합니다.
func FuzzParseID(f *testing.F) {
f.Add("user-123")
f.Add("")
f.Fuzz(func(t *testing.T, raw string) {
id, err := ParseID(raw)
if err == nil && id.String() == "" {
t.Fatalf("empty id from %q", raw)
}
})
}`go test -cover`, `-coverprofile`, `go tool cover`로 테스트가 실행한 statement를 확인하고 coverage 수치를 해석하는 기준을 정리합니다.
go test -cover ./...
go test -coverprofile=coverage.out ./...
go test -coverpkg=./... -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go tool cover -html=coverage.out`errors.Join`, `Unwrap() []error`, `errors.Is`, `errors.As`를 사용해 여러 실패를 하나의 error로 묶고 검사하는 기준입니다.
err := errors.Join(
closeFile(),
flushMetrics(),
shutdownServer(ctx),
)
if err != nil {
return err
}`testing.T`의 `Cleanup`, `TempDir`, `Helper`, `Setenv`로 테스트 fixture를 정리하고 실패 위치를 읽기 쉽게 만드는 기준입니다.
func TestLoadConfig(t *testing.T) {
srv := httptest.NewServer(handler())
t.Cleanup(srv.Close)
dir := t.TempDir()
t.Setenv("APP_CONFIG_DIR", dir)
writeFile(t, filepath.Join(dir, "config.json"), `{"debug":true}`)
cfg, err := LoadConfig()
if err != nil {
t.Fatal(err)
}
if !cfg.Debug {
t.Fatal("Debug = false")
}
}
func writeFile(t *testing.T, path, body string) {
t.Helper()
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}Go 동시성을 시작할 때 goroutine 생성, channel 통신, `context` 취소 전파를 한 흐름으로 어떻게 묶어 읽어야 하는지 정리합니다.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
jobs := make(chan int)
results := make(chan int)
go func() {
defer close(jobs)
jobs <- 21
}()
go func() {
worker(ctx, jobs, results)
close(results) // 이 예제에서는 worker가 하나라 이 goroutine이 닫습니다.
}()
for result := range results {
fmt.Println(result)
}`sync.WaitGroup`, `Mutex`, `RWMutex`, `atomic`을 언제 쓰고, goroutine 종료와 공유 상태 보호를 어떻게 나눠야 하는지 정리합니다.
Go에서 worker pool과 pipeline을 언제 쓰는지, channel close, `WaitGroup`, context 취소를 어떻게 같이 설계해야 하는지 정리합니다.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
jobs := make(chan Job)
results := make(chan Result)
// producer, worker, result consumer 모두 ctx.Done()을 함께 봅니다.`time.Sleep`, `Timer`, `Ticker`, timeout context를 언제 고르고, 반복 작업과 대기 흐름에서 어떤 정리 코드를 챙겨야 하는지 정리합니다.
time.Sleep(500 * time.Millisecond)
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()`select`로 여러 channel과 취소 신호를 기다리는 방식, channel close의 의미, 종료 소유권을 정리합니다.
for {
select {
case item, ok := <-jobs:
if !ok {
return
}
handle(item)
case <-ctx.Done():
return
}
}`context.WithTimeout`, `WithDeadline`, `WithCancelCause`, `context.Cause`로 취소 전파와 실패 원인을 구분하는 기준을 정리합니다.
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
if err := callBackend(ctx); err != nil {
return err
}`encoding/json`에서 exported field, struct tag, `omitempty`, `Marshal`/`Unmarshal`의 경계를 어떻게 읽어야 하는지 정리합니다.
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Nickname *string `json:"nickname,omitempty"`
Password string `json:"-"`
}`os`, `io`, `bufio`, `embed`를 언제 쓰고, 파일을 읽고 쓰는 코드에서 close와 경로 경계를 어떻게 잡아야 하는지 정리합니다.
data, err := os.ReadFile("config.json")
if err != nil {
return err
}
if err := os.WriteFile("out.txt", data, 0o644); err != nil {
return err
}`database/sql`에서 `sql.Open`, connection pool, context가 붙은 query, rows close를 어떤 기준으로 읽어야 하는지 정리합니다.
db, err := sql.Open("postgres", dsn)
if err != nil {
return err
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "select name from users where id = $1", id)
if err := row.Scan(&name); err != nil {
return err
}
rows, err := db.QueryContext(ctx, "select id, name from users")
if err != nil {
return err
}
defer rows.Close()`database/sql`에서 `BeginTx`, `Commit`, `Rollback`을 어떤 흐름으로 묶고, transaction 안에서 query를 어디에 붙여야 하는지 정리합니다.
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
return err
}
return tx.Commit()`sql.DB` connection pool에서 max open, max idle, connection lifetime, idle time, stats를 어떻게 조정하고 관찰할지 정리합니다.
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxIdleTime(5 * time.Minute)
db.SetConnMaxLifetime(30 * time.Minute)`net/http`의 `Handler`, `HandlerFunc`, `ServeMux`를 어떻게 읽고, middleware를 어떤 함수 조합으로 설계하는지 정리합니다.
mux := http.NewServeMux()
mux.HandleFunc("GET /health", health)
handler := logging(mux)
srv := &http.Server{Addr: ":8080", Handler: handler}
log.Fatal(srv.ListenAndServe())`http.Client`, request context, response body close, timeout 계층을 어떻게 나눠서 설계해야 하는지 정리합니다.
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()Go `http.Server`에서 종료 신호를 받고 새 연결을 막은 뒤 진행 중인 요청을 기다리는 graceful shutdown 흐름을 정리합니다.
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown failed: %v", err)
}`http.Server`의 timeout 필드와 `http.MaxBytesReader`로 느린 요청, 큰 body, header 지연을 어디서 제한할지 정리합니다.
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}`net/http` handler에서 JSON request body를 decode하고 JSON response를 encode할 때 body limit, Content-Type, status code를 정리합니다.
func createUser(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var req CreateUserRequest
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if err := dec.Decode(&struct{}{}); err != io.EOF {
http.Error(w, "JSON body must contain one value", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(CreateUserResponse{ID: "u_123"})
}`log`와 `log/slog`의 차이, 구조화 로그의 key-value 형태, handler와 context 로그를 어떤 기준으로 잡아야 하는지 정리합니다.
var level = new(slog.LevelVar) // 기본값: Info
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
}))
logger.With("service", "billing").InfoContext(ctx,
"request completed",
"method", r.Method,
"path", r.URL.Path,
"status", http.StatusOK,
)`os.Getenv`, `os.LookupEnv`, 문자열 파싱, 설정 struct를 어떻게 조합해 실행 환경별 설정을 읽는지 정리합니다.
rawPort, ok := os.LookupEnv("PORT")
if !ok || rawPort == "" {
rawPort = "8080"
}
port, err := strconv.Atoi(rawPort)
if err != nil {
return Config{}, fmt.Errorf("invalid PORT: %w", err)
}표준 `flag` 패키지로 옵션을 읽고, `FlagSet`으로 subcommand를 나누는 기본 구조를 정리합니다.
mytool serve -port 8080 -debug
mytool migrate -dry-run`go test -bench`, benchmark 함수, `pprof` 프로파일을 어떻게 읽고 성능 문제를 추적하는지 정리합니다.
go test -run=^$ -bench=. -benchmem ./...
go test -run=^$ -bench=BenchmarkEncode -cpuprofile cpu.out
go tool pprof -http=:0 cpu.out`go build`, `GOOS`, `GOARCH`, `CGO_ENABLED`를 어떻게 조합해 실행 파일을 만들고, cross compile에서 어디를 주의해야 하는지 정리합니다.
go build ./cmd/api
GOOS=linux GOARCH=amd64 go build -o bin/api-linux-amd64 ./cmd/api
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/api ./cmd/api`go test -race`로 data race를 어떻게 찾고, 공유 상태를 channel, mutex, atomic 중 무엇으로 보호해야 하는지 정리합니다.
go test -race ./...
go test -race ./internal/service
go run -race ./cmd/api