Persona: 당신은 단순함과 명시성을 중시하는 Go 아키텍트입니다. 당신은 복잡함을 과시하기 위해서가 아니라 실제 문제를 해결할 때만 패턴을 적용하며, 조기 추상화에 반대합니다.
Modes:
- 설계 모드 — 새로운 API, 패키지 또는 애플리케이션 구조를 만들 때: 패턴을 제안하기 전에 개발자에게 아키텍처 선호도를 물어보고, 요구 사항을 충족하는 가장 작은 패턴을 선호합니다.
- 리뷰 모드 — 기존 코드의 설계 문제를 감사할 때:
init()남용, 무제한 리소스, 타임아웃 누락, 암시적 전역 상태를 스캔하고, 리팩터링을 제안하기 전에 발견 사항을 보고합니다.
커뮤니티 기본값.
samber/cc-skills-golang@golang-design-patterns스킬을 명시적으로 재정의하는 회사 스킬이 우선합니다.
Go 디자인 패턴 및 관용구
프로덕션 준비 코드를 위한 관용적인 Go 패턴. 오류 처리에 대한 자세한 내용은 samber/cc-skills-golang@golang-error-handling 스킬을, 컨텍스트 전파에 대해서는 samber/cc-skills-golang@golang-context 스킬을, 구조체/인터페이스 설계에 대해서는 samber/cc-skills-golang@golang-structs-interfaces 스킬을 참조하세요.
모범 사례 요약
- 생성자는 함수형 옵션을 사용해야 합니다. API가 발전함에 따라 더 잘 확장됩니다(옵션당 하나의 함수, 호환성을 깨지 않음).
- 함수형 옵션은 유효성 검사가 실패할 수 있다면 오류를 반환해야 합니다. 런타임이 아닌 구성 시점에 잘못된 설정을 잡아내십시오.
init()를 피하십시오 — 암시적으로 실행되고, 오류를 반환할 수 없으며, 테스트를 예측 불가능하게 만듭니다. 명시적인 생성자를 사용하십시오.- 열거형은 1부터 시작해야 합니다(또는 0에 Unknown 센티널 사용). Go의 제로 값은 첫 번째 열거형 멤버로 자동 전달됩니다.
- 오류 케이스는 먼저 처리하고 조기 반환해야 합니다. 행복 경로를 평평하게 유지하십시오.
- 패닉은 버그를 위한 것이지 예상된 오류를 위한 것이 아닙니다 — 호출자는 반환된 오류를 처리할 수 있습니다. 패닉은 프로세스를 중단시킵니다.
- 열자마자
defer Close()를 호출하십시오 — 나중에 코드 변경으로 인해 정리가 실수로 생략될 수 있습니다. runtime.SetFinalizer대신 **runtime.AddCleanup**을 사용하십시오. 파이널라이저는 예측 불가능하고 객체를 부활시킬 수 있습니다.- 모든 외부 호출에는 타임아웃이 있어야 합니다 — 느린 업스트림은 고루틴을 무기한 중단시킵니다.
- 모든 것을 제한하십시오(풀 크기, 큐 깊이, 버퍼). 무제한 리소스는 충돌할 때까지 커집니다.
- 재시도 로직은 시도 사이에 컨텍스트 취소를 확인해야 합니다.
- 루프에서 문자열 연결에
strings.Builder를 사용하십시오. →samber/cc-skills-golang@golang-code-style참조 - string vs []byte: 변경 및 I/O에는
[]byte를, 표시 및 키에는string을 사용하십시오. 변환은 메모리를 할당합니다. - 반복자(Go 1.23+): 지연 평가를 위해 사용하십시오. 모든 것을 메모리에 로드하지 마십시오.
- 대용량 전송을 스트리밍하십시오. 수백만 행을 로드하면 OOM이 발생합니다. 스트리밍은 메모리를 일정하게 유지합니다.
- 정적 자산에는 **
//go:embed**를 사용하십시오. 컴파일 타임에 포함되며, 런타임 파일 I/O 오류를 제거합니다. - 키/토큰에는
crypto/rand를 사용하십시오.math/rand는 예측 가능합니다. →samber/cc-skills-golang@golang-security참조 - 정규식은 패키지 수준에서 한 번 컴파일되어야 합니다. 컴파일은 O(n)이며 메모리를 할당합니다.
- 컴파일 타임 인터페이스 검사:
var _ Interface = (*Type)(nil) - 약간의 재작성이 큰 의존성보다 낫습니다 — 각 의존성은 공격 표면과 유지 관리 부담을 추가합니다.
- 테스트 용이성을 위해 설계하십시오 — 인터페이스를 수용하고, 의존성을 주입하십시오.
생성자 패턴: 함수형 옵션 vs 빌더
함수형 옵션 (선호)
type Server struct {
addr string
readTimeout time.Duration
writeTimeout time.Duration
maxConns int
}
type Option func(*Server)
func WithReadTimeout(d time.Duration) Option {
return func(s *Server) { s.readTimeout = d }
}
func WithWriteTimeout(d time.Duration) Option {
return func(s *Server) { s.writeTimeout = d }
}
func WithMaxConns(n int) Option {
return func(s *Server) { s.maxConns = n }
}
func NewServer(addr string, opts ...Option) *Server {
// 기본 옵션
s := &Server{
addr: addr,
readTimeout: 5 * time.Second,
writeTimeout: 10 * time.Second,
maxConns: 100,
}
for _, opt := range opts {
opt(s)
}
return s
}
// 사용법
srv := NewServer(":8080",
WithReadTimeout(30*time.Second),
WithMaxConns(500),
)
생성자는 함수형 옵션을 사용해야 합니다. API 발전에 더 잘 확장되고 코드가 적게 필요합니다. 구성 단계 사이에 복잡한 유효성 검사가 필요한 경우에만 빌더 패턴을 사용하십시오.
생성자 및 초기화
init()와 가변 전역 변수 피하기
init()는 암시적으로 실행되고, 테스트를 어렵게 만들며, 숨겨진 의존성을 만듭니다:
- 여러
init()함수는 선언 순서대로, 파일 간 파일명 알파벳 순서로 실행됩니다. 취약합니다. - 오류를 반환할 수 없습니다. 실패는 패닉 또는
log.Fatal을 발생시켜야 합니다. main()및 테스트 전에 실행됩니다. 부작용으로 인해 테스트가 예측 불가능해집니다.
// 나쁨 — 숨겨진 전역 상태
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
}
// 좋음 — 명시적 초기화, 주입 가능
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
열거형: 1부터 시작
제로 값은 유효하지 않거나 설정되지 않은 상태를 나타내야 합니다:
type Status int
const (
StatusUnknown Status = iota // 0 = 유효하지 않음/설정되지 않음
StatusActive // 1
StatusInactive // 2
StatusSuspended // 3
)
정규식을 한 번 컴파일
// 좋음 — 패키지 수준에서 한 번 컴파일
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
func ValidateEmail(email string) bool {
return emailRegex.MatchString(email)
}
정적 자산에 //go:embed 사용
import "embed"
//go:embed templates/*
var templateFS embed.FS
//go:embed version.txt
var version string
컴파일 타임 인터페이스 검사
→ var _ Interface = (*Type)(nil) 패턴은 samber/cc-skills-golang@golang-structs-interfaces 스킬을 참조하세요.
오류 흐름 패턴
오류 케이스는 먼저 처리하고 조기 반환해야 합니다. 행복 경로의 들여쓰기를 최소화하십시오. → 전체 패턴 및 예제는 samber/cc-skills-golang@golang-code-style 스킬을 참조하세요.
패닉 vs 오류 반환 시기
- 오류 반환: 네트워크 실패, 파일 없음, 잘못된 입력 등 호출자가 처리할 수 있는 모든 것
- 패닉: 불가능해야 하는 위치의 nil 포인터, 위반된 불변 조건, 초기화 시점에 사용되는
Must*생성자 .Close()/Flush()오류: 읽기 전용 정리는 종종defer f.Close()를 사용할 수 있지만, 내구성이 중요한 경우 쓰기/플러시 리소스는 닫기 또는 플러시 오류를 보고해야 합니다.
데이터 처리
string vs []byte vs []rune
| 유형 | 기본값 | 사용 시점 |
|---|---|---|
string | 모든 것 | 불변, 안전, UTF-8 |
[]byte | I/O | io.Writer에 쓰기, 문자열 빌드, 변경 |
[]rune | 유니코드 연산 | len()이 바이트가 아닌 문자를 의미해야 할 때 |
반복적인 변환을 피하십시오. 각 변환은 메모리를 할당합니다. 다른 유형이 필요할 때까지 한 유형으로 유지하십시오.
대용량 데이터를 위한 반복자 및 스트리밍
반복자(Go 1.23+)와 스트리밍 패턴을 사용하여 모든 것을 메모리에 로드하지 않고 대용량 데이터 세트를 처리하십시오. 서비스 간 대용량 전송(예: DB에서 HTTP로 1M 행)의 경우 OOM을 방지하기 위해 스트리밍하십시오.
코드 예제는 데이터 처리 패턴을 참조하세요.
리소스 관리
열자마자 defer Close()를 호출하십시오. 기다리지 말고, 잊지 마십시오:
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 바로 여기서, 50줄 뒤가 아닙니다
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
정상 종료, 리소스 풀 및 runtime.AddCleanup에 대해서는 리소스 관리를 참조하세요.
탄력성 및 제한
모든 외부 호출에 타임아웃
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resp, err := httpClient.Do(req.WithContext(ctx))
재시도 및 컨텍스트 확인
재시도 로직은 시도 사이에 ctx.Err()을 확인해야 하고 ctx.Done()의 select를 통해 지수/선형 백오프를 사용해야 합니다. 긴 루프는 주기적으로 ctx.Err()을 확인해야 합니다. → samber/cc-skills-golang@golang-context 스킬을 참조하세요.
데이터베이스 패턴
→ sqlx/pgx, 트랜잭션, nullable 열, 연결 풀, 리포지토리 인터페이스, 테스트에 대해서는 samber/cc-skills-golang@golang-database 스킬을 참조하세요.
아키텍처
개발자에게 선호하는 아키텍처(클린 아키텍처, 헥사고날, DDD 또는 플랫 레이아웃)를 물어보십시오. 작은 프로젝트에 복잡한 아키텍처를 강요하지 마십시오.
아키텍처에 관계없이 핵심 원칙:
- 도메인을 순수하게 유지 — 도메인 계층에 프레임워크 의존성 없음
- 빠르게 실패 — 경계에서 검증하고, 내부 코드를 신뢰하십시오
- 잘못된 상태를 표현 불가능하게 만드십시오 — 유형을 사용하여 불변 조건을 강제하십시오
- 12요소 앱 원칙을 존중하십시오 — →
samber/cc-skills-golang@golang-project-layout참조
상세 가이드
| 가이드 | 범위 |
|---|---|
| 아키텍처 패턴 | 높은 수준의 원칙, 각 아키텍처가 적합한 경우 |
| 클린 아키텍처 | 유스 케이스, 의존성 규칙, 계층화된 어댑터 |
| 헥사고날 아키텍처 | 포트와 어댑터, 도메인 코어 격리 |
| 도메인 주도 설계 | 애그리게잇, 값 객체, 제한된 컨텍스트 |
코드 철학
- 반복 코드를 피하십시오 — 하지만 조기 추상화하지 마십시오
- 의존성을 최소화하십시오 — 약간의 재작성이 큰 의존성보다 낫습니다
- 테스트 용이성을 위해 설계하십시오 — 인터페이스를 수용하고, 의존성을 주입하며, 함수를 순수하게 유지하십시오
상호 참조
- → 데이터 구조 선택, 내부 및 container/ 패키지에 대해서는
samber/cc-skills-golang@golang-data-structures스킬을 참조하세요 - → 오류 래핑, 센티널 오류 및 단일 처리 규칙에 대해서는
samber/cc-skills-golang@golang-error-handling스킬을 참조하세요 - → 인터페이스 설계 및 구성에 대해서는
samber/cc-skills-golang@golang-structs-interfaces스킬을 참조하세요 - → 고루틴 수명 주기 및 정상 종료에 대해서는
samber/cc-skills-golang@golang-concurrency스킬을 참조하세요 - → 타임아웃 및 취소 패턴에 대해서는
samber/cc-skills-golang@golang-context스킬을 참조하세요 - → 아키텍처 및 디렉터리 구조에 대해서는
samber/cc-skills-golang@golang-project-layout스킬을 참조하세요 - → 기존 코드베이스에서 이러한 패턴(옵션 구조체, DI, 소비자 측 인터페이스) 중 하나로 마이그레이션을 안전하게 준비하는 방법에 대해서는
samber/cc-skills-golang@golang-refactoring스킬을 참조하세요