실전 고수 팁 — 에러 처리
Go의 에러 처리 철학
package main
import (
"errors"
"fmt"
)
// 철학 1: 에러는 값이다 (Errors are values)
// 에러를 특별히 취급하지 말고 일반 값처럼 다룬다
type Result[T any] struct {
Value T
Err error
}
func NewResult[T any](val T, err error) Result[T] {
return Result[T]{Value: val, Err: err}
}
func (r Result[T]) IsOK() bool { return r.Err == nil }
func (r Result[T]) Unwrap() (T, error) { return r.Value, r.Err }
// 철학 2: 실패를 처리하라, 무시하지 마라
func badStyle() {
// Bad: 에러 무시
// result, _ := someOperation()
// 에러가 발생해도 알 수 없음
}
// 철학 3: 에러 컨텍스트를 풍부하게
var ErrNotFound = errors.New("not found")
// Bad: 어디서 발생했는지 모름
func badGetUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "user", nil
}
// Good: 컨텍스트 포함
func goodGetUser(id int) (string, error) {
if id <= 0 {
return "", fmt.Errorf("GetUser(id=%d): %w", id, ErrNotFound)
}
return "user", nil
}
func main() {
// 에러를 값으로 다루기
results := []Result[int]{
NewResult(42, nil),
NewResult(0, fmt.Errorf("calculation failed: %w", ErrNotFound)),
}
for _, r := range results {
val, err := r.Unwrap()
if err != nil {
fmt.Println("에러:", err)
if errors.Is(err, ErrNotFound) {
fmt.Println("→ 찾을 수 없음")
}
} else {
fmt.Println("값:", val)
}
}
// 컨텍스트 비교
_, err1 := badGetUser(-1)
_, err2 := goodGetUser(-1)
fmt.Printf("\n나쁜 에러: %v\n좋은 에러: %v\n", err1, err2)
}
if err != nil 줄이기 패턴
package main
import (
"fmt"
"strconv"
)
// 패턴 1: errWriter — io.Writer와 유사하게 에러를 한번에 처리
type errWriter struct {
data []byte
err error
}
func (ew *errWriter) writeString(s string) {
if ew.err != nil {
return // 이미 에러가 있으면 스킵
}
ew.data = append(ew.data, s...)
}
func (ew *errWriter) writeInt(n int) {
if ew.err != nil {
return
}
ew.data = append(ew.data, strconv.Itoa(n)...)
}
func (ew *errWriter) validate(cond bool, msg string) {
if ew.err != nil {
return
}
if !cond {
ew.err = fmt.Errorf("validation: %s", msg)
}
}
func buildPayload(name string, age int) ([]byte, error) {
ew := &errWriter{}
ew.validate(name != "", "name is required")
ew.validate(age >= 0, "age must be non-negative")
ew.validate(age <= 150, "age is too large")
ew.writeString(`{"name":"`)
ew.writeString(name)
ew.writeString(`","age":`)
ew.writeInt(age)
ew.writeString(`}`)
return ew.data, ew.err
}
// 패턴 2: 함수형 체이닝
type Pipeline[T any] struct {
value T
err error
}
func NewPipeline[T any](val T) *Pipeline[T] {
return &Pipeline[T]{value: val}
}
func (p *Pipeline[T]) Then(fn func(T) (T, error)) *Pipeline[T] {
if p.err != nil {
return p
}
p.value, p.err = fn(p.value)
return p
}
func (p *Pipeline[T]) Result() (T, error) {
return p.value, p.err
}
// 패턴 3: defer로 에러 처리 통합
func processWithDefer(id int) (result string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("processWithDefer(id=%d): %w", id, err)
}
}()
if id <= 0 {
return "", fmt.Errorf("invalid id")
}
return fmt.Sprintf("processed-%d", id), nil
}
func main() {
// errWriter 패턴
fmt.Println("=== errWriter 패턴 ===")
data, err := buildPayload("Alice", 30)
if err == nil {
fmt.Println("페이로드:", string(data))
}
_, err = buildPayload("", -1)
if err != nil {
fmt.Println("유효성 실패:", err)
}
// Pipeline 패턴
fmt.Println("\n=== Pipeline 패턴 ===")
result, err := NewPipeline(" hello world ").
Then(func(s string) (string, error) {
if s == "" {
return "", fmt.Errorf("empty string")
}
// 트림
trimmed := ""
for i, c := range s {
if c != ' ' {
if trimmed == "" {
trimmed = s[i:]
}
}
}
return "trimmed: " + s, nil
}).
Then(func(s string) (string, error) {
return "[" + s + "]", nil
}).
Result()
if err != nil {
fmt.Println("파이프라인 에러:", err)
} else {
fmt.Println("파이프라인 결과:", result)
}
// defer 에러 래핑
fmt.Println("\n=== defer 에러 래핑 ===")
for _, id := range []int{1, -1} {
res, err := processWithDefer(id)
if err != nil {
fmt.Println("에러:", err)
} else {
fmt.Println("결과:", res)
}
}
}
slog를 이용한 구조화 로깅 연동
package main
import (
"errors"
"fmt"
"log/slog"
"os"
)
// 에러 코드 타입
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error { return e.Err }
// slog와 에러 연동 패턴
func logError(logger *slog.Logger, err error, msg string, args ...any) {
if err == nil {
return
}
// AppError에서 코드 추출
var appErr *AppError
if errors.As(err, &appErr) {
logger.Error(msg,
append(args,
"error", err,
"code", appErr.Code,
"cause", appErr.Err,
)...,
)
return
}
logger.Error(msg, append(args, "error", err)...)
}
var ErrNotFound = errors.New("not found")
func fetchUser(id int) (string, error) {
if id <= 0 {
return "", &AppError{Code: 400, Message: "invalid user id"}
}
if id > 100 {
return "", &AppError{Code: 404, Message: "user not found", Err: ErrNotFound}
}
return fmt.Sprintf("User-%d", id), nil
}
func main() {
// JSON 형식의 구조화 로거
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
// 텍스트 형식 (개발용)
textLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
ids := []int{1, 0, 999}
for _, id := range ids {
user, err := fetchUser(id)
if err != nil {
logError(logger, err, "사용자 조회 실패", "user_id", id)
logError(textLogger, err, "사용자 조회 실패", "user_id", id)
} else {
logger.Info("사용자 조회 성공", "user_id", id, "user", user)
}
}
}
에러 처리 안티패턴과 올바른 패턴
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
// ====== 안티패턴 ======
// 안티패턴 1: 에러 무시
func antiPattern1() {
val := 0
_ = val // 에러를 완전히 무시
// 올바른 방법: 에러를 처리하거나 상위로 전파
}
// 안티패턴 2: panic을 일반 에러 제어로 사용
func antiPattern2(id int) string {
if id <= 0 {
panic("invalid id") // Bad! 일반 에러는 error 반환으로
}
return fmt.Sprintf("item-%d", id)
}
// 안티패턴 3: 에러를 문자열로 비교
func antiPattern3(err error) bool {
return err.Error() == "not found" // Bad! 문자열 비교는 깨지기 쉬움
}
// 안티패턴 4: 에러 컨텍스트 없이 그냥 전파
func antiPattern4() error {
err := ErrNotFound
return err // Bad! 어디서 발생했는지 알 수 없음
}
// ====== 올바른 패턴 ======
// 올바른 패턴 1: 에러 전파 + 컨텍스트
func goodPattern1(id int) (string, error) {
if id <= 0 {
return "", fmt.Errorf("goodPattern1: invalid id %d: %w", id, ErrNotFound)
}
return fmt.Sprintf("item-%d", id), nil
}
// 올바른 패턴 2: errors.Is로 비교
func goodPattern2(err error) bool {
return errors.Is(err, ErrNotFound) // Good!
}
// 올바른 패턴 3: 에러 타입 확인은 errors.As
type ValidationError struct{ Field, Msg string }
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation[%s]: %s", e.Field, e.Msg)
}
func goodPattern3(err error) {
var ve *ValidationError
if errors.As(err, &ve) { // Good! 타입 안전 추출
fmt.Printf("필드 %q 유효성 실패: %s\n", ve.Field, ve.Msg)
}
}
// 올바른 패턴 4: 에러 조기 반환 (early return)
func goodPattern4(a, b, c int) (int, error) {
// 각 단계에서 즉시 반환 — 들여쓰기 줄이기
if a <= 0 {
return 0, fmt.Errorf("a must be positive: %d", a)
}
if b <= 0 {
return 0, fmt.Errorf("b must be positive: %d", b)
}
if c <= 0 {
return 0, fmt.Errorf("c must be positive: %d", c)
}
return a + b + c, nil
}
// 올바른 패턴 5: 에러 그룹화 처리 (Go 1.20+)
func goodPattern5(ids []int) error {
var errs []error
for _, id := range ids {
if id <= 0 {
errs = append(errs, fmt.Errorf("invalid id: %d", id))
}
}
return errors.Join(errs...)
}
func main() {
// 패턴 비교
fmt.Println("=== 에러 전파 ===")
_, err := goodPattern1(-1)
fmt.Println("에러:", err)
fmt.Println("Is ErrNotFound:", goodPattern2(err))
fmt.Println("\n=== 타입 안전 에러 확인 ===")
ve := &ValidationError{Field: "email", Msg: "invalid format"}
wrapped := fmt.Errorf("process: %w", ve)
goodPattern3(wrapped)
fmt.Println("\n=== early return ===")
result, err := goodPattern4(1, 0, 3)
if err != nil {
fmt.Println("에러:", err)
} else {
fmt.Println("결과:", result)
}
fmt.Println("\n=== 에러 그룹화 ===")
joinErr := goodPattern5([]int{1, -1, 0, 2, -3})
if joinErr != nil {
fmt.Println("복합 에러:", joinErr)
}
fmt.Println("\n=== 요약 ===")
fmt.Println("1. 에러를 무시하지 말 것 (_ 사용 최소화)")
fmt.Println("2. 비교는 errors.Is, 타입 추출은 errors.As")
fmt.Println("3. %w로 래핑하여 컨텍스트 보존")
fmt.Println("4. 일반 에러에 panic 사용 금지")
fmt.Println("5. early return으로 들여쓰기 최소화")
}
핵심 규칙
- 에러는 값이다— 특별 취급 말고 일반 값처럼 처리
- 에러를 무시하지 마라—
_로 버리는 것은 버그의 시작%w로 컨텍스트 추가— 어디서 왜 실패했는지 기록errors.Is/errors.As— 문자열 비교 대신 타입 안전 비교- panic은 최후의 수단— 일반 에러 상황에 사용 금지
- 조기 반환(early return)— 들여쓰기를 줄이고 가독성 향상
- slog로 구조화 로깅— 에러 코드와 컨텍스트를 함께 기록