본문으로 건너뛰기

에러 기초

Go에서 에러 처리는 예외(exception)가 아닌 반환값 으로 이루어집니다. 함수가 실패할 수 있으면 마지막 반환값으로 error를 반환하는 것이 Go의 관례입니다. 이 단순한 설계가 코드를 예측 가능하고 명시적으로 만들어 줍니다.

error 인터페이스

error는 Go 내장 인터페이스로, 단 하나의 메서드만 가집니다.

package main

import (
"errors"
"fmt"
)

// error 인터페이스 정의 (Go 표준 라이브러리)
// type error interface {
// Error() string
// }

// errors.New — 가장 간단한 에러 생성
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")

func findUser(id int) (string, error) {
users := map[int]string{
1: "Alice",
2: "Bob",
}
name, ok := users[id]
if !ok {
return "", ErrNotFound
}
return name, nil
}

func main() {
// 성공 케이스
name, err := findUser(1)
if err != nil {
fmt.Println("에러:", err)
} else {
fmt.Println("사용자:", name) // 사용자: Alice
}

// 실패 케이스
name, err = findUser(99)
if err != nil {
fmt.Println("에러:", err) // 에러: not found
} else {
fmt.Println("사용자:", name)
}

// 에러 비교 (sentinel error)
if err == ErrNotFound {
fmt.Println("사용자를 찾을 수 없습니다") // 사용자를 찾을 수 없습니다
}
}

fmt.Errorf — 포맷팅된 에러 메시지

fmt.Errorf는 동적인 정보를 담은 에러 메시지를 만들 때 사용합니다.

package main

import (
"fmt"
)

func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide by zero: %v / %v", a, b)
}
return a / b, nil
}

func openFile(path string) error {
// 시뮬레이션: 특정 경로에서 실패
if path == "" {
return fmt.Errorf("openFile: path cannot be empty")
}
if path == "/etc/shadow" {
return fmt.Errorf("openFile %q: permission denied", path)
}
return nil
}

func main() {
// divide 사용
result, err := divide(10, 0)
if err != nil {
fmt.Println("나누기 실패:", err) // 나누기 실패: divide by zero: 10 / 0
} else {
fmt.Println("결과:", result)
}

result, err = divide(10, 3)
if err != nil {
fmt.Println("나누기 실패:", err)
} else {
fmt.Printf("결과: %.4f\n", result) // 결과: 3.3333
}

// openFile 사용
for _, path := range []string{"", "/etc/shadow", "/tmp/data.txt"} {
if err := openFile(path); err != nil {
fmt.Println("파일 열기 실패:", err)
} else {
fmt.Printf("파일 열기 성공: %q\n", path)
}
}
// 파일 열기 실패: openFile: path cannot be empty
// 파일 열기 실패: openFile "/etc/shadow": permission denied
// 파일 열기 성공: "/tmp/data.txt"
}

에러 반환 관례

Go에서 에러를 반환할 때 지켜야 할 관례들입니다.

package main

import (
"errors"
"fmt"
)

// 관례 1: 에러는 항상 마지막 반환값
func readConfig(path string) (string, error) {
if path == "" {
return "", errors.New("config path is required")
}
return "config content", nil
}

// 관례 2: 성공 시 nil 반환
func processData(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("data is empty")
}
result := make([]byte, len(data))
copy(result, data)
return result, nil
}

// 관례 3: 에러가 있으면 다른 반환값은 의미 없음 (보통 zero value)
func parseInt(s string) (int, error) {
if s == "" {
return 0, errors.New("empty string") // 0은 의미 없는 zero value
}
var n int
for _, c := range s {
if c < '0' || c > '9' {
return 0, fmt.Errorf("invalid character: %c", c)
}
n = n*10 + int(c-'0')
}
return n, nil
}

// 관례 4: 에러 변수 이름은 err (단일), err1/err2 또는 의미있는 이름 (복수)
func multipleOperations() error {
_, err := readConfig("app.yaml")
if err != nil {
return err
}

_, err = processData([]byte("data"))
if err != nil {
return err
}

return nil
}

// 관례 5: 패키지 수준 센티넬 에러는 Err 접두사
var (
ErrEmpty = errors.New("empty input")
ErrInvalid = errors.New("invalid input")
ErrTimeout = errors.New("operation timed out")
)

func validate(input string) error {
if input == "" {
return ErrEmpty
}
if len(input) > 100 {
return ErrInvalid
}
return nil
}

func main() {
// 패턴 1: 바로 if err != nil 처리
config, err := readConfig("app.yaml")
if err != nil {
fmt.Println("설정 로드 실패:", err)
return
}
fmt.Println("설정:", config)

// 패턴 2: 인라인 에러 처리
if n, err := parseInt("123"); err != nil {
fmt.Println("파싱 실패:", err)
} else {
fmt.Println("파싱 성공:", n) // 파싱 성공: 123
}

if _, err := parseInt("12a3"); err != nil {
fmt.Println("파싱 실패:", err) // 파싱 실패: invalid character: a
}

// 패턴 3: 센티넬 에러 비교
errs := []string{"", "valid", "too_long_input_exceeding_hundred_characters_limit_yes_this_is_very_long_string_here_to_test"}
for _, s := range errs {
if err := validate(s); err != nil {
switch err {
case ErrEmpty:
fmt.Println("빈 입력입니다")
case ErrInvalid:
fmt.Println("잘못된 입력입니다")
default:
fmt.Println("알 수 없는 에러:", err)
}
} else {
fmt.Printf("유효한 입력: %q\n", s)
}
}
}

에러와 nil 처리

에러를 올바르게 전파하고 처리하는 패턴입니다.

package main

import (
"errors"
"fmt"
)

var ErrNotFound = errors.New("not found")
var ErrDBConn = errors.New("database connection failed")

// 계층적 에러 전파 — 각 레이어는 에러를 그대로 위로 전달
type DB struct{ connected bool }

func (db *DB) Query(id int) (string, error) {
if !db.connected {
return "", ErrDBConn
}
if id <= 0 {
return "", ErrNotFound
}
return fmt.Sprintf("record-%d", id), nil
}

type Repository struct{ db *DB }

func (r *Repository) FindByID(id int) (string, error) {
record, err := r.db.Query(id)
if err != nil {
return "", err // 에러를 그대로 위로 전파
}
return record, nil
}

type Service struct{ repo *Repository }

func (s *Service) GetUser(id int) (string, error) {
user, err := s.repo.FindByID(id)
if err != nil {
return "", err // 에러를 그대로 위로 전파
}
return "User: " + user, nil
}

func main() {
// 정상 동작
db := &DB{connected: true}
repo := &Repository{db: db}
svc := &Service{repo: repo}

user, err := svc.GetUser(42)
if err != nil {
fmt.Println("에러:", err)
} else {
fmt.Println(user) // User: record-42
}

// DB 연결 실패
db2 := &DB{connected: false}
repo2 := &Repository{db: db2}
svc2 := &Service{repo: repo2}

_, err = svc2.GetUser(42)
if err != nil {
fmt.Println("에러:", err) // 에러: database connection failed
if errors.Is(err, ErrDBConn) {
fmt.Println("→ DB 연결 에러 감지") // → DB 연결 에러 감지
}
}

// 레코드 없음
_, err = svc.GetUser(-1)
if err != nil {
fmt.Println("에러:", err) // 에러: not found
if errors.Is(err, ErrNotFound) {
fmt.Println("→ 레코드 없음") // → 레코드 없음
}
}
}

여러 에러 수집

여러 작업에서 발생한 에러를 모아서 처리하는 패턴입니다.

package main

import (
"errors"
"fmt"
"strings"
)

// MultiError — 여러 에러를 하나로 모으는 타입
type MultiError struct {
Errors []error
}

func (m *MultiError) Add(err error) {
if err != nil {
m.Errors = append(m.Errors, err)
}
}

func (m *MultiError) Error() string {
msgs := make([]string, len(m.Errors))
for i, err := range m.Errors {
msgs[i] = err.Error()
}
return strings.Join(msgs, "; ")
}

func (m *MultiError) Unwrap() []error {
return m.Errors
}

func (m *MultiError) HasError() bool {
return len(m.Errors) > 0
}

// 유효성 검사 — 모든 오류를 수집해서 한 번에 반환
type UserInput struct {
Name string
Email string
Age int
}

func validateUser(u UserInput) error {
var merr MultiError

if u.Name == "" {
merr.Add(errors.New("name is required"))
} else if len(u.Name) < 2 {
merr.Add(errors.New("name must be at least 2 characters"))
}

if u.Email == "" {
merr.Add(errors.New("email is required"))
} else if !strings.Contains(u.Email, "@") {
merr.Add(errors.New("email must contain @"))
}

if u.Age < 0 {
merr.Add(errors.New("age cannot be negative"))
} else if u.Age > 150 {
merr.Add(errors.New("age is too large"))
}

if merr.HasError() {
return &merr
}
return nil
}

func main() {
inputs := []UserInput{
{Name: "Alice", Email: "alice@example.com", Age: 30},
{Name: "", Email: "invalid", Age: -1},
{Name: "B", Email: "bob@example.com", Age: 200},
}

for _, input := range inputs {
err := validateUser(input)
if err != nil {
fmt.Printf("유효성 검사 실패 (%+v):\n %v\n", input, err)

// MultiError로 타입 단언
var merr *MultiError
if errors.As(err, &merr) {
fmt.Printf(" 총 %d개 에러\n", len(merr.Errors))
}
} else {
fmt.Printf("유효한 입력: %+v\n", input)
}
}
}

핵심 정리

  • error는 단 하나의 메서드(Error() string)를 가진 인터페이스
  • errors.New로 정적 에러, fmt.Errorf로 동적 에러 생성
  • 에러는 항상 마지막 반환값으로, 성공 시 nil 반환
  • 패키지 수준 센티넬 에러는 Err 접두사 사용 (예: ErrNotFound)
  • 에러를 무시하지 말 것 — _로 버리는 것은 위험