Go: 관용적 효율성 참조
목차
1. 에러 처리 {#errors}
// ❌ 에러 무시
result, _ := os.Open(path)
// ✅ — 항상 처리; 에러가 명백히 무관한 경우에만 _ 사용
result, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
// ❌ 중복되는 에러 변수
err := doA()
if err != nil { return err }
err = doB()
if err != nil { return err }
// ✅ — 각각 :=/:= 사용도 괜찮습니다. 이것은 Go의 관용적 표현입니다. "고치려" 하지 마세요.
// 단순화할 수 있는 부분: if 본문이 단일 return인 경우 한 줄로 축약 가능
if err := doA(); err != nil { return err }
if err := doB(); err != nil { return err }
// ❌ 부가 가치가 없는 사용자 정의 에러 타입
type MyError struct{ msg string }
func (e MyError) Error() string { return e.msg }
// ✅ — 호출자가 타입을 검사해야 하는 경우가 아니라면 errors.New나 fmt.Errorf 사용
var ErrNotFound = errors.New("not found")
return fmt.Errorf("lookup %q: %w", key, ErrNotFound)
호출자가 errors.Is / errors.As를 사용할 수 있도록 에러는 %v 대신 %w로 감싸세요.
2. 슬라이스 & 맵 {#slices}
// ❌ 크기를 알면서도 사전 할당 없이 슬라이스를 늘리는 경우
var result []string
for _, item := range items {
result = append(result, item.Name)
}
// ✅
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.Name)
}
// ❌ 맵 쓰기 전에 수동 존재 확인
if _, ok := m[key]; !ok {
m[key] = []string{}
}
m[key] = append(m[key], value)
// ✅ — nil 슬라이스에 append 유효
m[key] = append(m[key], value)
// ❌ 할당으로 맵 복사 (참조를 복사)
copy := original
// ✅
copy := make(map[K]V, len(original))
for k, v := range original { copy[k] = v }
3. 고루틴 & 채널 {#concurrency}
// ❌ 생명 주기 없는 발사 후 망각 고루틴
go doWork()
// ✅ — errgroup 또는 WaitGroup을 사용하여 완료 추적
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
// ❌ 불필요한 고루틴 블록킹을 유발하는 버퍼 없는 채널
ch := make(chan Result)
go func() { ch <- compute() }()
result := <-ch
// ✅ — 단일 결과의 경우, 버퍼 있는 채널을 사용하면 수신자가 일찍 종료해도 고루틴 누수 방지
ch := make(chan Result, 1)
go func() { ch <- compute() }()
result := <-ch
// ❌ 바쁜 대기 default가 있는 select
for {
select {
case v := <-ch:
process(v)
default:
// 회전
}
}
// ✅ — 진정으로 논블로킹이 필요한 경우가 아니라면 블로킹 select 사용
for v := range ch {
process(v)
}
에러 수집이 필요한 팬아웃에는 golang.org/x/sync/errgroup을 사용하세요.
4. 구조체 & 인터페이스 {#structs}
// ❌ 큰 인터페이스
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
// ... 10개 메서드 더
}
// ✅ — 작고 조합 가능한 인터페이스
type Getter interface { Get(key string) ([]byte, error) }
type Setter interface { Set(key string, val []byte) error }
type Storage interface { Getter; Setter }
// ❌ 생성자에서 구체적인 구조체 반환 (호출자를 구현에 종속시킴)
func NewStore() *RedisStore { ... }
// ✅ — 여러 구현체가 있거나 예상될 때는 인터페이스 반환
func NewStore() Storage { return &RedisStore{...} }
// ❌ 작은 값 타입에 포인터 리시버 사용
func (p *Point) X() float64 { return p.x }
// ✅ — 작고 불변인 타입에는 값 리시버 사용
func (p Point) X() float64 { return p.x }
규칙: 메서드가 상태를 변경하거나 구조체가 큰 경우(비교적 큰 크기의 필드 3개 이상)에는 포인터 리시버, 그 외에는 값 리시버를 사용합니다.
5. 함수 & 클로저 {#functions}
// ❌ 변수 선언을 피하기 위해 명명된 반환값 사용
func divide(a, b float64) (result float64, err error) {
result = a / b
return
}
// ✅ — 명명된 반환값은 지연된 변경이나 문서화 목적으로만 가치 있음
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
// ❌ 루프 변수를 캡처하는 클로저 (고전적인 Go 버그, Go 1.22+에서 수정됨)
// 1.22 미만: 각 고루틴이 같은 i를 캡처
for i := 0; i < n; i++ {
go func() { use(i) }()
}
// ✅ (Go <1.22 — 매개변수로 전달)
for i := 0; i < n; i++ {
go func(i int) { use(i) }(i)
}
// Go 1.22+: 루프 변수가 반복마다 스코프가 지정되어 원래 코드도 안전함
6. Go 특화 반대 패턴 {#antipatterns}
| 반대 패턴 | 권장 사항 |
|---|---|
if err != nil { return err }가 5회 이상 반복 | 허용 가능 — Go의 관용적 표현입니다 |
예상 가능한 에러에 panic | return err |
부수 효과가 있는 init() | main이나 생성자에서 명시적 초기화 |
제네릭 없이 interface{} / any | 제네릭(Go 1.18+) 또는 타입 인터페이스 사용 |
| Mutex 필드가 보호하는 데이터와 인접하지 않음 | mu를 보호하는 필드 바로 위에 배치 |
| 채널의 채널 | 보통 과도한 엔지니어링 신호; 재설계 필요 |
테스트에서 time.Sleep | 동기화를 위해 testing 훅 또는 채널 사용 |
| 필드가 중요할 때 비공개 필드를 가진 내보내진 타입 | 모든 필드가 공개인 record 스타일 구조체 |
main 외부에서 log.Fatal | 에러를 호출 스택 위로 반환 |
제한 사항
- 이는 언어별 가이드라인이며 전반적인 아키텍처 결정을 다루지 않습니다.
- 지나친 압축은 가독성을 떨어뜨릴 수 있습니다; 판단력을 발휘하세요.