프로그래밍 언어 & 프레임워크/Golang

Go: 에러와 패닉 (panic, recover)

예외가 없다고?

그렇다. Go 언어에는 예외(Exception)가 없다. 에러로 모든 것을 처리한다. error 빌트인 타입은 존재하긴 하지만, 그렇다고 타언어처럼 모든 에러에 대해 타입이 매핑되어 있는 것은 아니다. 내가 접한 언어 중에는 Syntax, Out of Range 등을 전부 나눠놓은 경우가 있었는데, 그다지 좋은 기분이 들지는 않았다.

 

아니면 에러와 예외를 둘 다 사용하여 컴파일이나 정적으로 발생하는 것은 에러로, 런타임 중에 발생한 것은 예외로 하여 개별 클래스가 계층에 따라 분리된 경우도 있었는데, 상당히 헷갈리기 그지 없었다.

 

Go 에는 에러패닉이 있는데, 패닉은 런타임 중에 발생하는 치명적인 에러를 의미하며 프로그램이 더 이상 진행될 수 없는 수준의 에러다. 상당히 치명적이어서 패닉이 발생하면 프로그램이 종료된다. 반면 일반적인 에러로는 프로그램이 종료되지 않는다. 패닉은 일반적으로 개발자가 미처 대처하지 못한 알려지지 않은 예외에 대해 적용된다.

에러

일반적인 에러는 발생해도 프로그램이 종료되지 않는다. 예를 들어 아래와 같이 파일을 열 때, 에러도 반환하는데, 일반적으로는 아래와 같이 처리한다. 조건문을 사용하고 에러가 nil 이 아닌지 검사하는 것이다. 일반적으로 에러가 발생하면 errnil 이 아닌 error 타입의 값이 할당된다.

package main

import (
	"log"
	"os"
)

func main() {
	_, err := os.Open("Unknown.txt")
	if err != nil {
		log.Fatal(err)
	}
}

error interface

error 는 빌트인 타입, 정확히 말하자면 인터페이스다. Error() 메서드를 구현한 구조체라면 error 타입으로 활용이 가능하다. error 인터페이스를 구현하게 되면 나만의 에러를 만드는 것도 어려운 일이 아니다.

$ go doc builtin error
package builtin // import "builtin"

type error interface {
        Error() string
}
    The error built-in interface type is the conventional interface for
    representing an error condition, with the nil value representing no error.

패닉

패닉은 런타임 중에 발생한 치명적인 에러다. 아래의 코드는 배열에서 인덱스를 넘은 값에 접근하려고 하자 발생한 패닉이다. 에러는 처리해줄 수도 있고 안 해줄 수도 있지만, 일반적인 경우라는 패닉은 발생하는 일이 없어야한다. 알려지지 않은 경우에 대해 발생하는 패닉은 아래에서 나오는 방법으로 어느정도 제어가 가능하다.

package main

import "fmt"

func main() {
	var numbers [1]int
	for i := 0; i <= len(numbers); i++ {
		// -> panic: runtime error: index out of range [1] with length 1
		fmt.Println(numbers[i])
	}
}

panic()

타언어에서 예외를 던지듯이 패닉또한 개발자가 패닉이 필요하다고 느끼는 시점에서 빌트인 함수인 panic() 함수를 통해 던질 수 있다. 패닉이 발생하면 현재 실행중인 고루틴을 멈춘다. 또한 패닉에서 중요한 점은 패닉이 발생해도 defer 를 통해 예약해둔 함수는 호출이 보장된다는 점이다.

package main

import "fmt"

func main() {
	defer fmt.Println("Hello, Go!")
	// -> panic: PANIC
	panic("PANIC")
}

여기서 주의해야 할 점은, 패닉이 발생한 시점에서 즉시 멈추기 때문에 panic() 함수 이후에 있는 구문은 실행되지 않는다는 점이다.

recover()

recover() 함수를 사용하면 패닉이 발생해도 회복이 가능하다. 위에서 defer 를 통해 함수를 예약해두면 패닉이 발생해도 실행을 해준다고 했다. deferrecover() 가 포함되어있는 함수를 호출하게 되면 패닉을 정상적인 상태로 되돌릴 수 있다. 이렇게 패닉을 회복하고, recover() 함수는 패닉을 호출할 때 넘겨준 파라매터를 리턴값으로 되돌려준다. 만약 패닉이 발생하지 않았다면 리턴 값은 nil 이다.

package main

import "fmt"

func main() {
	defer func() {
		// -> PANIC
		fmt.Println(recover())
	}()
	panic("PANIC")
}

recover() 를 통하면 발생하는 모든 패닉에 대해서는 유연하게 대처할 수 있겠으나, 정말로 예상하지 못한 에러에 대해서는 패닉을 제대로 발생시켜줄 필요가 있다. 따라서 패닉을 한 번 더 쓰는것도 좋다.

package main

import (
	"errors"
	"fmt"
)

func main() {
	defer func() {
		p := recover()
		if p == nil {
			return
		}
		err, ok := p.(error)
		if ok {
			fmt.Printf("%#v\n", err)
			return
		}
		panic(p)
	}()
	panic(errors.New("ERROR"))
}