Go에서의 에러 생성의 미묘한 기술: errors.New와 fmt.Errorf 이해하기
Emily Parker
Product Engineer · Leapcell

Go의 에러 처리 방식은 관용적입니다: 에러는 단순한 값입니다. 이 단순함은 강력하고 유연한 시스템을 숨기고 있지만, 모든 강력한 도구와 마찬가지로 효과적으로 사용하려면 이해가 필요합니다. 에러를 생성하는 두 가지 기본적인 함수는 errors.New
와 fmt.Errorf
입니다. 둘 다 error
인터페이스를 반환하지만, 의도된 사용 사례와 기본 기능은 상당히 다릅니다. 이러한 차이점을 이해하는 것은 견고하고 유지보수 가능하며 디버깅 가능한 Go 애플리케이션을 작성하는 데 중요합니다.
errors.New
: 단순하고 불투명한 에러
errors.New
함수는 기본적인 에러를 생성하는 가장 간단한 방법입니다. 단일 문자열 인수를 받아 해당 문자열을 반환하는 Error()
메서드를 구현하는 error
값을 반환합니다.
package main import ( "errors" "fmt" ) func validateInput(input string) error { if input == "" { return errors.New("input cannot be empty") } if len(input) > 20 { return errors.New("input exceeds max length of 20 characters") } return nil } func main() { if err := validateInput(""); err != nil { fmt.Println("Error:", err) } if err := validateInput("This is a very long input string that will definitely exceed the limit."); err != nil { fmt.Println("Error:", err) } if err := validateInput("valid input"); err == nil { fmt.Println("Input is valid.") } }
errors.New
를 사용하는 경우:
-
센티널 에러 (Sentinel Errors): 주요 사용 사례는 호출자가 직접적인 동등성(
==
)을 사용하여 확인할 수 있는 "센티널 에러" – 즉, 특정하고 미리 정의된 에러 값 – 를 정의하는 것입니다. 이러한 에러는 매우 구체적인 유형의 실패를 전달합니다.package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("item not found") var ErrPermissionDenied = errors.New("permission denied") func findItem(id int) (string, error) { if id == 1001 { return "", ErrNotFound } if id == 2002 { return "", ErrPermissionDenied } return fmt.Sprintf("Item-%d", id), nil } func main() { if _, err := findItem(1001); err != nil { if errors.Is(err, ErrNotFound) { fmt.Println("Client error: The requested item was not found.") } else if errors.Is(err, ErrPermissionDenied) { fmt.Println("Authentication error: You do not have permission.") } else { fmt.Println("Unexpected error:", err) } } }
이 예시에서
ErrNotFound
와ErrPermissionDenied
는 정확한 에러 처리를 가능하게 하는 고유한 센티널 에러입니다. Go 1.13에 도입된errors.Is
함수는 센티널 에러를 확인하는 표준적인 방법으로, 래핑된 에러를 올바르게 처리합니다 (이는fmt.Errorf
와 함께 더 자세히 설명됩니다). -
간단하고 고정된 에러: 에러 메시지에 동적 정보가 필요하지 않고 고정된 경우.
errors.New
의 한계:
- 컨텍스트 부족: 고정된 문자열만 제공합니다. 에러 발생 시점의 특정 값 (예: 에러를 유발한 특정 값, 열 수 없었던 파일 경로 또는 이 에러로 이어진 원래 에러)과 같은 동적 정보를 포함할 수 없습니다.
- 에러 래핑 불가:
errors.New
로는 하위 에러를 래핑할 수 없어, 원래의 호출 스택과 실패 컨텍스트를 잃게 됩니다. 이는 디버깅을 훨씬 더 어렵게 만듭니다.
fmt.Errorf
: 동적이고, 컨텍스트를 포함하며, 래핑 가능한 에러
fmt.Errorf
함수는 에러를 생성하는 더 강력하고 다재다능한 방법입니다. fmt.Sprintf
와 유사하게 작동하여 다양한 동사를 사용하여 에러 문자열을 포맷할 수 있습니다. 결정적으로, %w
동사를 에러 래핑에 지원합니다.
package main import ( "errors" "fmt" "os" ) func readFile(filepath string) ([]byte, error) { data, err := os.ReadFile(filepath) if err != nil { // 여기서 os.ReadFile에서 반환된 하위 에러를 래핑합니다. // %w 동사는 'err'이 래핑된 에러임을 나타냅니다. return nil, fmt.Errorf("failed to read file '%s': %w", filepath, err) } return data, nil } func processFile(filename string) error { _, err := readFile(filename) if err != nil { // 이 계층에서 더 많은 컨텍스트를 추가하여 다시 래핑할 수 있습니다. return fmt.Errorf("error processing data from %s: %w", filename, err) } return nil } func main() { // 파일 없음 에러 시뮬레이션 if err := processFile("non_existent_file.txt"); err != nil { fmt.Println("Main handler received error:", err) // 전체 에러 체인을 출력합니다. // 원래 에러가 os 패키지의 "not found" 에러인지 확인합니다. var pathErr *os.PathError if errors.As(err, &pathErr) { fmt.Printf("Specifically, it was a PathError for path: %s, operation: %s\n", pathErr.Path, pathErr.Op) } // 특정 센티널 에러가 체인에 있는지 확인합니다 (깊이 중첩되어 있어도). if errors.Is(err, os.ErrNotExist) { fmt.Println("The ultimate cause was that the file did not exist.") } } // 동적 에러 메시지를 보여주는 또 다른 예 userID := 123 dbErr := errors.New("database connection failed") err := fmt.Errorf("failed to fetch user %d data: %w", userID, dbErr) fmt.Println(err) }
fmt.Errorf
를 사용하는 경우:
-
동적 에러 메시지: 에러 메시지에 특정 값 (예: 잘못된 입력 값, 실패한 리소스 ID)을 포함해야 할 때.
fmt.Errorf("invalid age %d; must be positive", age)
-
에러 래핑 (
%w
): 이것이 가장 중요한 차이점입니다. 함수에서 종속성 (다른 함수 호출, 라이브러리, OS)의 에러를 접했을 때, 추가 컨텍스트를 제공하기 위해 해당 에러를 래핑해야 합니다. 래핑을 통해 원래의 에러 체인을 보존할 수 있으며, 이는 디버깅에 매우 유용합니다.errors.Is(err, target)
:err
또는err
체인의 어떤 에러라도target
인지 확인합니다. 센티널 에러와 비교할 때 이상적입니다.errors.As(err, &target)
:err
을 언래핑하여target
에 할당 가능한 에러를 찾을 때까지 반복합니다. 특정 에러 유형 (예:*os.PathError
,*net.OpError
)을 확인하고 거기서 정보를 추출하는 데 유용합니다.errors.Unwrap(err)
:err
이 래핑한 하위 에러를 반환하거나,err
이 에러를 래핑하지 않으면nil
을 반환합니다. 이는 주로errors.Is
및errors.As
에서 내부적으로 사용됩니다.
에러 래핑의 이점:
- 근본 원인 보존: 최종 실패로 이어진 전체 에러 시퀀스를 추적할 수 있습니다.
- 컨텍스트 정보: 호출 스택의 각 계층은 자체 컨텍스트 정보를 추가하여 원래 세부 정보를 잃지 않고 에러 메시지를 풍부하게 만들 수 있습니다.
- 프로그램식 검사: 상위 레벨 코드는 에러 문자열을 파싱할 필요 없이 (
errors.Is
또는errors.As
를 사용하여) 특정 저수준 에러를 여전히 확인할 수 있습니다. 이를 통해 더 견고하고 덜 취약한 에러 처리 로직을 구현할 수 있습니다.
errors.New
와 fmt.Errorf
중 선택하기
간단한 의사 결정 트리입니다:
- 이 특정 에러 값을
errors.Is()
를 사용하여 확인해야 합니까?- 예:
errors.New
(예:var ErrInvalidInput = errors.New("invalid input")
)를 사용하여var
를 정의합니다.
- 예:
- 에러 메시지에 동적 정보 (예: 특정 값, ID)를 포함해야 합니까?
- 예:
fmt.Errorf
를 사용합니다.
- 예:
- 하위 레벨 함수 또는 종속성에서 에러를 전파하고 있습니까?
- 예: 하위 에러를 래핑하기 위해
%w
와 함께fmt.Errorf
를 사용합니다.
- 예: 하위 에러를 래핑하기 위해
- 기존 에러를 래핑하지 않고 완전히 새로운 에러를 만들고 있으며 메시지가 고정되어 있습니까?
- 예: 단순화를 위해
errors.New
를 사용할 수 있지만,fmt.Errorf("static error message")
도 습관적으로 자주 사용되며 전혀 문제가 없습니다. 나중에 동적 콘텐츠를 추가하거나 래핑할 가능성이 조금이라도 있다면, 향후 리팩토링을 위해fmt.Errorf
가 더 안전합니다.
- 예: 단순화를 위해
일반적인 지침:
대부분의 경우, 특히 에러가 애플리케이션의 여러 계층을 통해 전파될 때 fmt.Errorf
를 선호하십시오. 컨텍스트를 추가하고 하위 에러를 래핑하는 기능은 fmt.Errorf
가 errors.New
에 비해 갖는 약간의 오버헤드를 훨씬 능가하는 강력한 디버깅 도구입니다. errors.New
는 주로 애플리케이션 로직 내에서 명시적으로 확인되는 var
센티널 에러를 정의하는 데 사용하십시오.
에러 메시지에 대한 모범 사례
- 소문자로 시작: 에러 메시지는 일반적으로 대문자로 시작하지 않으며 구두점으로 끝나지 않습니다.
fmt.Println
이 줄 바꿈 및 잠재적으로 다른 서식을 추가하기 때문입니다.- 좋음:
return fmt.Errorf("failed to open file '%s'", filename)
- 나쁨:
return errors.New("Failed to open file.")
- 좋음:
- 간결하지만 유익하게: 무엇이 잘못되었는지 이해하는 데 충분한 정보를 제공하되, 너무 장황하지 않게 하십시오.
- 중복 피하기: 에러를 래핑할 때, 외부 에러 메시지는 내부 에러를 단순히 반복하는 것이 아니라 새로운 컨텍스트를 추가해야 합니다.
- 좋음:
fmt.Errorf("failed to process request for user %d: %w", userID, err)
- 나쁨:
fmt.Errorf("error: %s", err)
(컨텍스트를 추가하지 않고 하위 에러를 단순히 반복합니다)
- 좋음:
- 에러는 주로 기계(코드)를 위한 것이지, 사람을 위한 것이 아닙니다: 명확하게 읽을 수 있지만, 에러 객체의 주요 소비자는 종종 오류를 기반으로 결정을 내리는 스택의 더 높은 코드입니다. 상세한 메시지는 디버깅을 위한 것입니다. 사용자 대면 메시지는 종종 내부 에러를 더 친근한 메시지에 매핑하여 별도로 생성해야 합니다.
결론
errors.New
와 fmt.Errorf
모두 Go의 에러 처리에서 필수적인 도구입니다. errors.New
는 직접 비교를 위한 간단하고 고정된 센티널 에러를 정의하는 데 뛰어나지만, 포맷팅 기능과, 더 중요하게는 에러 래핑 (%w
) 지원을 갖춘 fmt.Errorf
는 풍부하고 컨텍스트를 갖춘 디버깅 가능한 에러 체인을 만드는 데 핵심적인 역할을 합니다. 이러한 함수를 적절하게 활용하고 모범 사례를 따르면, Go 개발자는 에러 처리에서 견고할 뿐만 아니라 디버깅 및 유지보수가 훨씬 쉬운 애플리케이션을 구축할 수 있습니다. 의미 있는 에러를 만드는 미묘한 기술은 기계가 반응하고 사람이 문제의 근본 원인을 이해하는 데 필요한 충분한 정보를 제공하는 데 있습니다.