개요
이번 블로그 포스트에서는 Golang에서 에러를 다루는 방법에 대해서 알아보도록 하겠습니다. 이 블로그 포스트에서 소개하는 코드는 다음 링크를 통해 확인하실 수 있습니다.
에러 핸들링
프로그래밍에서 에러는 언제 어디서나 발생할 수 있는 문제입니다.
- 버그: 휴먼 에러(프로그래머의 실수), 프로그램의 오동작
- 외부 환경: 머신의 문제(메모리 부족), 물리적인 문제(전원 차단)
이런 에러를 처리할 때, 두 가지 방법이 있습니다. 하나는 실행중인 프로그램을 종료시키는 것과, 또 하나는 에러를 처리(Handling)하고 프로그램을 지속시키는 방법이 있습니다.
에러 반환
Golang에서 다음과 같이 에러를 반환하여, 코드를 사용하는 쪽에서 에러를 다룰 수 있도록 합니다.
import "os"
file, err := os.Open(filename)
file, err := os.Create(filename)
이를 확인하기 위해서 main.go
파일을 생성하고 다음과 같이 수정합니다.
package main
import (
"bufio"
"fmt"
"os"
)
func ReadFile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", err
}
defer file.Close()
rd := bufio.NewReader(file)
line, _ := rd.ReadString('\n')
return line, nil
}
func WriteFile(filename string, data string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = fmt.Fprintf(file, data)
return err
}
const filename string = "data.txt"
func main() {
line, err := ReadFile(filename)
if err != nil {
err = WriteFile(filename, "Hello, World!\n")
if err != nil {
fmt.Println("Failed to create file!", err)
return
}
line, err = ReadFile(filename)
if err != nil {
fmt.Println("Failed to read file!", err)
return
}
}
fmt.Println("File contents: ", line)
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
File contents: Hello, World!
이처럼 함수를 생성하고 제공할 때, 에러를 반환하도록 하여 사용하는 쪽에서 에러를 처리하도록 하는 것이 좋습니다.
사용자 에러 반환
Golang에서는 다음과 같이 사용자 에러(Custom error)를 만들 수 있습니다.
fmt.Errorf(formatter string, ...interface {})
또는, 다음과 같이 errors
패키지를 사용하여 생성할 수 있습니다.
import "errors"
errors.New(text string) error
이를 확인하기 위해, 다음과 같이 main.go
파일을 수정합니다.
package main
import (
"errors"
"fmt"
)
func errorTest(i int) (int, error) {
switch i {
case 0:
return i, fmt.Errorf("Error: %d", i)
case 1:
return i, errors.New("Error")
default:
return i, nil
}
}
func main() {
i, err := errorTest(0)
if err != nil {
fmt.Println(err)
}
i, err = errorTest(1)
if err != nil {
fmt.Println(err)
}
i, err = errorTest(2)
if err != nil {
fmt.Println(err)
}
fmt.Println(i)
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
Error: 0
Error
2
보통, 에러 메시지에 특정 변수값을 출력하고 싶을 때에는 fmt.Errorf
를 사용하고, 단순히 문자열 에러 메시지를 출력할 때는 errors.New
를 사용합니다.
에러 타입
에러 메시지에 여러 정보들을 추가하고 출력하고 싶을 때, 에러 타입을 사용할 수 있습니다.
type error interface {
Error() string
}
Golang에서는 Error
메서드를 구현하는 객체를 에러 타입으로 사용할 수 있습니다. 예를 들어 다음과 같이 PasswordError
타입을 정의하고, Error
메소드를 구현하면, 에러 타입으로 사용할 수 있습니다.
type PasswordError struct {
RequiredLen int
Len int
}
func (e PasswordError) Error() string {
return "Password is too short."
}
이를 확인하기 위해, main.go
파일을 다음과 같이 수정합니다.
package main
import (
"fmt"
)
type PasswordError struct {
RequiredLen int
Len int
}
func (e PasswordError) Error() string {
return "Password is too short."
}
func RegisterAccount(name, password string) error {
if len(password) < 8 {
return PasswordError{RequiredLen: 8, Len: len(password)}
}
// register account logic
// ...
return nil
}
func main() {
err := RegisterAccount("john", "12345")
if err != nil {
if errInfo, ok := err.(PasswordError); ok {
fmt.Printf("%v (Required: %d Len: %d)\n", errInfo, errInfo.RequiredLen, errInfo.Len)
}
}
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
Password is too short. (Required: 8 Len: 5)
이처럼 에러에 여러 부가 정보를 추가할 때, 에러 타입을 사용하게 됩니다.
에러 랩핑
Golang에서는 에러를 랩핑(Wrapping)하여 여러 에러들을 추가하여 하나의 에러를 만들 수 있습니다. 에러 랩핑에는 다음과 같이 fmt.Errorf
을 사용합니다.
fmt.Errorf("%w", err)
또한, errors
패키지의 Is
와 As
를 사용하여, 랩핑된 에러인지 확인하거나 변환하여 사용할 수 있습니다.
errors.Is(err, StrError)
errors.As(err, &strError)
이를 확인하기 위해, main.go
파일을 다음과 같이 수정합니다.
package main
import (
"errors"
"fmt"
)
type StrError struct {
Msg string
}
func (e *StrError) Error() string {
return fmt.Sprintf("Message: %s", e.Msg)
}
func msgError(msg string) error {
return &StrError{Msg: msg}
}
func WrapError(msg string) error {
err := msgError(msg)
return fmt.Errorf("(Wrapping) %w", err)
}
func main() {
err1 := msgError("Error")
fmt.Println("[Normal Error]", err1)
err2 := WrapError("Test Error Message")
fmt.Println("[Wrapping Error]", err2)
var strErr *StrError
if errors.As(err2, &strErr) {
fmt.Printf("[Failed] %s\n", strErr)
}
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
[Normal Error] Message: Error
[Wrapping Error] (Wrapping) Message: Test Error Message
[Failed] Message: Test Error Message
이와 같이 Golang에서는 에러를 랩핑하여 하나의 에러로 처리할 수 있습니다.
패닉
Golang에서는 패닉(Panic)을 사용하여 에러와 함께 프로그램을 종료시킬 수 있습니다. 패닉은 프로그램을 빠르게 종료시켜, 프로그래머가 오류를 인식하게 하고 오류를 해결하도록 유도하도록 합니다.
이를 확인하기 위해 main.go
파일을 다음과 같이 수정합니다.
package main
import "fmt"
func divide(a, b int) {
if b == 0 {
panic("divide by zero")
}
fmt.Printf("%d / %d = %d\n", a, b, a/b)
}
func main() {
divide(4, 2)
divide(4, 0)
divide(4, 3)
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
4 / 2 = 2
panic: divide by zero
goroutine 1 [running]:
main.divide(0x4, 0x2)
/study-golang/error-handling/5.panic/main.go:7 +0x10b
main.main()
/study-golang/error-handling/5.panic/main.go:14 +0x31
exit status 2
두번째 divide
함수 호출에서 패닉이 발생하여 프로그램이 종료되었고, 그로 인해 세번째 divide
함수가 호출되지 않았음을 알 수 있습니다.
패닉은 다음과 같이 어떠한 타입도 전달 받을 수 있습니다.
func panic(v interface{})
따라서 다음과 같이 패닉을 사용할 수 있습니다.
panic(42)
panic("error")
panic(fmt.Errorf("error num: %d", 42))
panic(SomeType{SomeData})
패닉 전파 그리고 복구
프로그램을 개발할 때에는 문제를 빠르게 파악하고 해결하는 것이 중요합니다. 따라서 패닉을 사용하여 문제가 발생하였을 때, 프로그램을 종료시키고 빠르게 문제를 해결할 수 있도록 하는 것이 좋습니다.
하지만, 개발한 프로그램을 외부에 공개하고 서비스할 때에는 프로그램을 최대한 종료되지 않게 만들어야 합니다. 조금만 사용하면 프로그램이 강제 종료된다면 어느 누구도 그 프로그램을 사용하려 하지 않을 것입니다.
Golang은 이를 위해 패닉을 복구하는 recover
를 제공하고 있습니다.
func recover() interface {}
recover
는 defer
와 함께 사용하여 패닉을 복구할 수 있습니다. 그럼 이를 확인하기 위해 main.go
파일을 다음과 같이 수정합니다.
package main
import (
"fmt"
)
func firstCall() {
fmt.Println("(2) firstCall function start")
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in firstCall", r)
}
}()
group()
fmt.Println("(2) firstCall function end")
}
func group() {
fmt.Println("(3) group function start")
fmt.Printf("4/2 = %d\n", divide(4, 2))
fmt.Printf("4/0 = %d\n", divide(4, 0))
fmt.Println("(3) group function end")
}
func divide(a, b int) int {
if b == 0 {
panic("divide by zero")
}
return a / b
}
func main() {
fmt.Println("(1) main function start")
firstCall()
fmt.Println("(1) main function end")
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
(1) main function start
(2) firstCall function start
(3) group function start
4/2 = 2
Recovered in firstCall divide by zero
(1) main function end
패닉을 복구하기 위해 recover
를 사용한 함수는 firstCall
임을 알 수 있습니다. 패닉이 발생하는 곳은 divide
함수입니다. 이처럼 패닉은 divide() > group() > firstCall() > main()
순으로 전파됩니다. 여기서는 firstCall
함수에서 복구를 하였으므로, main
함수까지는 전파되지 않고 firstCall
에서 복구되는 것을 확인할 수 있습니다.
프로그램을 개발할 때에는 복구를 사용하지 않는 것을 추천드립니다. 복구를 사용하게 된다면 프로그램이 죽지 않기 때문에 문제의 원인을 찾거나 문제를 인식하는 것이 늦어질 수 있습니다.
배포된 상태에서는 복구를 사용할 수 밖에 없습니다. 그럼 위와 같이 문제의 원인을 찾거나 인식하는 것이 늦어질 수 있으므로, 패닉이 발생하면 이메일로 알리거나 디비에 저장하여 이를 빨리 인식할 수 있도록 하는 것이 좋습니다.
Golang은 SHE를 지원하지 않는다
Golang은 구조화된 에러 처리(SHE, Structured Error Handling)를 지원하지 않습니다.
try {
...
} catch (Exception ex) {
...
} finally {
...
}
다른 프로그래밍 언어에서는 try-catch
문을 사용하여 에러를 처리하지만, Golang은 이를 지원하지 않습니다. SHE는 다음과 같은 문제점을 가지고 있습니다.
- 성능 문제: 정상일 때에도 이 구조를 가지기 위해 성능이 저하된다.
- 에러를 인지하기 어렵다. 이로 인해 에러 처리를 하지 않거나 방치하는 경우가 발생한다.
완료
이것으로 Golang에서 에러를 처리하는 방법에 대해서 알아보았습니다. 프로그램을 지속하면서 에러를 발생시키는 방법과 패닉을 이용해 프로그램을 종료시키는 방법, 그리고 패닉을 복구하는 방법에 대해서 알아보았습니다.
에러는 프로그래밍에서 굉장히 중요한 역할을 합니다. 그러므로 에러를 반환하는 함수가 있다면, 빈칸지시자(_
)로 무시하지 말고 꼭 처리를 하는 것이 좋습니다. 패닉 역시 프로그램의 종료를 막기 위해 recover
를 사용하는 것은 좋지만, 이 문제를 인식하고 처리할 수 있는 장치(이메일, 디비 등)를 함께 사용해야 합니다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku
가 개발한 앱을 한번 사용해보세요.Deku
가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.