[Go] 16. 에러 핸들링

2024. 3. 10. 17:04Go

반응형

1. 에러 반환

에러가 발생할 수 있는 함수의 경우, 함수 내에서 에러를 처리하지 말고, 다중 리턴 성징을 활용하여 에러를 함께 반환하는 것을 추천합니다.

해당 메서드에서 에러가 발생했을 경우, 어떤 에러가 발생된 것인지 알림과 함께 종료 또는 재개를 하도록 코드를 작성하면 좋습니다.

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, line string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = fmt.Fprintf(file, line)
	return err
}

func main() {
	filename := "data.txt"
	line, err := ReadFile(filename)
	if err != nil {
		err = WriteFile(filename, "파일이 존재하지 않아서 WriteFile 함수로 새로 생성했습니다.")
		if err != nil {
			fmt.Println("파일 생성중 에러 발생", err)
			return
		}
		line, err = ReadFile(filename)
		if err != nil {
			fmt.Println("파일 읽기 2번째 실패", err)
			return
		}
	}
	fmt.Println("파일에 쓰여진 값은", line)

}

1.1. 사용자 정의 에러 반환

fmt.Errorf() 함수를 이용하여 사용자 정의 에러를 반환할 수 있습니다.

package main

import (
	"fmt"
	"math"
)

func Sqrt(f float64) (float64, error) {
	if f < 0 {
		return 0, fmt.Errorf("제곱근은 양수여야 합니다. 입력값: %f\n", f)
	}
	return math.Sqrt(f), nil
}

func f1(num float64) {
	result, err := Sqrt(num)
	if err != nil {
		fmt.Printf("Error! %v\n", num)
		return
	}
	fmt.Printf("Sqrt(%f) = %v\n", num, result)
}

func main() {
	f1(2.2)
	f1(-2.2)
}
Sqrt(2.200000) = 1.4832396974191326
Error! -2.2

errors 패키지의 New를 이용하려 error 타입을 생성할수도 있습니다.

import "errors"
errors.New("제곱근은 양수여야 합니다")

2. 에러 타입

error 타입은 인터페이스로, string을 리턴하는 Error() 메서드의 선언만 가지고 있습니다.
즉, Error() 함수를 가지고 있다면, 에러로 사용할 수 있습니다.

type error interface {
	Error() string
}

package main

import "fmt"

type PasswordError struct {
	Len         int
	RequiredLen int
}

func (err PasswordError) Error() string {
	return "암호길이가 짧습니다."
}

type Account struct {
	Id       string
	Password string
}

func RegisterAccount(id, password string) (*Account, error) {
	const minimumLength = 8
	if len(password) < minimumLength {
		return nil, PasswordError{len(password), minimumLength}
	}
	return &Account{id, password}, nil
}

func main() {
	if account, err := RegisterAccount("jini", "jini123"); err != nil {
		if errInfo, ok := err.(PasswordError); ok {
			fmt.Printf("최소 길이: %d, 입력 길이: %d\n", errInfo.RequiredLen, errInfo.Len)
		}
	} else {
		fmt.Println("회원가입 성공! ", account)
	}
}
최소 길이: 8, 입력 길이: 7

PasswordError는 Error() 메서드를 구현하고 있기 때문에, err를 타입변환할 수 있습니다.

err.(PasswordError)


3. 에러 랩핑


4. panic

panic은 프로그램을 정상 진행시키기 어려운 상황을 만났을 때, 프로그램 흐름을 중지시키는 기능입니다.

Go에서는 내장함수 panic()으로 패닉 기능을 제공합니다.

func panic(v any)

panic() 내장함수는 인자값으로 모든 타입을 받을 수 있습니다.

위에서 설명했었던 error 인터페이스를 활용한 예제들은, 호출자에게 에러가 발생된 이유를 직접적으로 알려주는 것이었다면,
패닉은 프로그램 수행시 예기치 못한 에러를 만났을 때 사용하는 기능입니다.

  • 버그 발생으로 인해 잘못된 메모리에 접근하거나
  • 메모리가 부족할 때

아래와 같이 특정 조건에 panic을 발생시킬 수 있습니다.
panic() 함수를 호출하면, 에러 메시지를 출력하고 call stack을 표시하는데, 이것을 통해 에러가 발생된 경로를 알 수 있습니다.

package main

import "fmt"

func divide(a, b int) {
	if b == 0 {
		panic("분모 b는 0일 수 없습니다")
	}
	fmt.Printf("%d / %d = %d\n", a, b, a/b)
}

func main() {
	divide(3, 0)
}
panic: 분모 b는 0일 수 없습니다

goroutine 1 [running]:
main.divide(0x3?, 0x2?)
        /Users/sherry/jini_box/jinispaces/go/goplayground/ch23/ex23.5/ex23.5.go:7 +0xc8
main.main()
        /Users/sherry/jini_box/jinispaces/go/goplayground/ch23/ex23.5/ex23.5.go:14 +0x30

5. 패닉 복구

recover() 함수를 이용하면, 패닉 복구를 할 수 있습니다.

func recover() any

recover() 함수를 실행했을 때, 패닉 전파중일 경우에는 panic객체를 반환하고, 아닐 경우에는 nil을 반환합니다.
아래의 분기문은, 패닉전파시에만 실행되는 구문입니다.

if r := recover(); r != nil {
	fmt.Println("panic 복구 - ", r)
}

recover 함수를 이용하여, 패닉 전파를 복구해봅시다.
아래와 같이, defer 지연 실행을 활용하면, f() 함수 실행 종료 직전에 특정 구문을 실행되도록 할 수 있습니다.

package main

import "fmt"

func f() {
	fmt.Println("f() start")
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("panic 복구 - ", r)
		}
	}()
	g()
	fmt.Println("f() finish")
}

func g() {
	fmt.Printf("3 / 2 = %d\n", divide(3, 2))
	//fmt.Printf("3 / 0 = %d\n", divide(3, 0))
}

func divide(a, b int) int {
	if b == 0 {
		panic("분모 b는 0일 수 없습니다")
	}
	return a / b
}

func main() {
	f()
	fmt.Println("main 끝부분")
}

panic을 발생시키는 코드를 담고 있는 예제코드입니다.
만일, 위의 코드에서 패닉을 유발시키지 않는다면 f() 함수도 정상적으로 종료한 후, main 끝부분 출력까지 마무리됩니다.

f() start
3 / 2 = 1
f() finish
main 끝부분

여기에서, g() 함수 내의 주석부분을 해제하면, g() 함수를 종료하기 전에 panic이 발생되게 됩니다.
이 경우, 패닉 복구가 실행되어, panic 복구 - 분모 b는 0일 수 없습니다이 출력되게 됩니다.
g() 함수 도중에 패닉이 발생되었기 때문에, f() 함수는 정상적으로 종료되지 않기 때문에, f() finish는 출력되지 않습니다.
이렇게 패닉 복구를 잘 활용하면 서비스가 도중에 종료되지 않습니다!

f() start
3 / 2 = 1
panic 복구 -  분모 b는 0일 수 없습니다
main 끝부분

panic함수의 인자로 any타입을 넣을 수 있었던 것 처럼, recover함수의 결과도 any타입입니다.
특정 타입의 패닉일 경우에만 어떤 행위를 동작하게 하고 싶다면 아래와 같이 작성할 수도 있습니다.

if r, ok := recover().(net.Error); ok {
	fmt.Println("net.Error 타입의 패닉에 대한 복구")
}
728x90
반응형

'Go' 카테고리의 다른 글

[Go] 18. 채널  (0) 2024.03.12
[Go] 17. 고루틴  (0) 2024.03.11
[Go] 15. 자료구조 - list, ring, map  (0) 2024.02.05
[Go] 14. 함수 고급편  (0) 2024.02.04
[Go] 13. 인터페이스  (0) 2024.01.21