[Go] panic , recover

김무연·2025년 2월 10일

GoLang의 문법 뿌시기

목록 보기
12/14

라이브러리 루틴은 호출자(caller)에게 어떤 상황의 에러라 해도 자주 리턴해 주어야한다. Go의 특징인 다중값 리턴은 일반적인 리턴값에 비해 상세한 에러 내용을 제공하기 쉽게 만들어준다.

상세한 에러 내용을 제공하기 위해 이러한 특징을 활용해야 할 것이다. os.Open의 메서드 같은경우 실패할 경우 단순히 nil 만을 리턴하지 않고, 에러에 관한 내용까지 return 해 준다.

지난 포스팅에서 보았던 인터페이스 에러 타입을 이용해

type error interface {
	Error() string
}

이를 이용해 자유롭게 구현하면서, 에러를 보여줄 뿐 아니라 맥락을 함께 제공 가능하다. os.Open 은 *os.File 값을 리턴하는 동시에 에러값 또한 리턴해준다.

// PathError는 에러와 연산, 문제를 발생시킨 파일 경로를 가지고 있다.
type PathError struct {
    Op string    // "open", "unlink", 등.
    Path string  // 관련 파일.
    Err error    // 시스템 콜에 의해 리턴됨.
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

PathError 의 Error 메서드는 다음과 같은 문자열을 생성한다.

open /etc/passwx: no such file or directory

패닉 (panic)

호출자(caller)에게 에러를 알려주는 일반적인 방법은 에러를 부가적인 값으로 리턴하는 것이다. Read 메서드는, 바이트 수와 에러를 리턴한다. 그러나 복구할 수 없는 에러라면 상황이 다를 것이다. 프로그램이 더 이상 진행이 안 될것이다.

이런 목적을 위해 Go에는 런타임 에러를 일으켜 프로그램을 종료시키는 panic 이라는 내장함수가 있다.
이 함수는 프로그램이 종료되었을 때 출력될 임의의 타입 하나를 인자로 받는다. (대개 string)
이는 또한 어떤 불가능한 일이 벌어졌음을 알리는 방법이기도 한데, 무한 루프에서 탈출하는 것이 예가 될 수 있겠다.

package panictest

import (
	"fmt"
)

func CubeRoot(x int) {
	ctn := 0
	defer fmt.Println("이거 실행되나?")
	for i := 0; i < x; i++ {
		ctn++
		fmt.Println(ctn)

		if ctn == 100 {
			panic("종료")
		}
	}



}

func Panictest() {
	CubeRoot(1000)
}

또한 panic() 함수는 현재 함수를 즉시 멈추고 현재 함수에 defer 함수들을 모두 실행한 후 즉시 리턴한다. 이러한 panic 모드 실행 방식은 다시 상위함수에도 똑같이 적용되고, 계속 콜스택을 타고 올라가며 적용된다. 그리고 마지막에는 프로그램이 에러를 내고 종료하게 된다.

panic: 종료

goroutine 1 [running]:
GOSTUDY/panictest.CubeRoot(0x3e8)
c:/Users/its/Desktop/GoStudy/panictest/panictest.go:15 +0x1c5
GOSTUDY/panictest.Panictest()
c:/Users/its/Desktop/GoStudy/panictest/panictest.go:24 +0x18
main.main()
c:/Users/its/Desktop/GoStudy/main.go:110 +0x2a

허나 실제 라이브러리 함수는 panic을 피해야 한다. 만약 문제가 숨어있거나 주변에서 문제를 발생시킨다해도, 실행을 계속 진행하는 것이 전체 프로그램을 종료하는 것보다 낫다.

한 가지 가능한 반례는, 초기화 작업 중, 만약 라이브러리가 초기 셋업을 못하게 된다면, 이는 패닉을 통해 종료시키고 알려야 할 것이다.

var user = os.Getenv("USER")

func init() {
	if user == "" {
    	panic("no value for $USER env")
    }
}

복구 (Recover)

슬라이스 범위를 벗어난 인덱싱이나 타입 단언 실패, 런타임 에러를 포함한 패닉이 발생하였을 때, 이는 즉시 현재 함수의 실행을 중단 시키며 모든 지연된(deferred) 함수를 실행하면서 고루틴 스택을 풀기 시작한다.

만약 풀기 작업이 고루틴 스택의 최정상에 도달했을 때, 프로그램은 종료된다. 그러나 내장함수인 recover 를 사용하면 고루틴의 통제권을 다시 얻을 수 있으며, 명령어 실행을 정상적으로 진행할 수 있게 된다.

recover 를 호출하면 풀기 작업이 중지되며, panic 에 전달된 인자값이 리턴된다. 풀기 작업을 하는 동안에 실행되는 코드는 오직 지연된 함수안에서만 실행되기 대문에, recover 는 오직 지연된 함수 내에서만 유용하다.

recover 의 한 응용 사례 중 하나는 서버내의 실행중인 다른 고루틴들은 죽이지 않고, 오직 실패한 고루틴을 종료시키는 것이다.

package recovertest

import (
	"fmt"
	"os"
)

func openFile(fn string) {
	defer func() {
		if r:= recover(); r != nil {
			fmt.Println("OPEN ERROR : ", r)
		}
	}()

	f, err := os.Open(fn)
	if err != nil {
		panic(err)
	}

	fmt.Println(f.Name())

	defer f.Close()
}

func Recovertest() {
	// 여기서 오류가 발생했지만, recover에 의해 복구된 후
	openFile("/error.txt")

	// 정상적으로 아래의 코드 진행
	fmt.Println("Done")

}

recover 는 지연된 함수로부터 직접 호출되는 경우를 제외하면 항상 nil을 return 하기 대문에, 지연된 코드는 panic과 recover 를 사용하는 라이브러리 루틴을 실패없이 호출할 수 있다.

profile
Notion에 정리된 공부한 글을 옮겨오는 중입니다... (진행중)

0개의 댓글