
Defer, Panic, and Recover
앤드루 게런드(Andrew Gerrand)
2010년 8월 4일
Go에는 if, for, switch, goto 같은 일반적인 제어 흐름 구문이 있다. 별도의 고루틴에서 코드를 실행하는 go 문도 있다. 여기서는 덜 흔히 쓰이는 defer, panic, recover를 살펴본다.
defer 문은 함수 호출을 목록에 저장한다. 저장된 호출은 이를 감싼 함수가 반환된 뒤 실행된다. defer는 여러 정리 작업을 하는 함수를 단순하게 만드는 데 흔히 쓰인다. 예를 들어 한 파일을 열어 다른 파일에 내용을 복사하는 함수를 보자.
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } dst, err := os.Create(dstName) if err != nil { return } written, err = io.Copy(dst, src) dst.Close() src.Close() return }
이 함수에는 버그가 있다. os.Create 호출이 실패하면 소스 파일을 닫지 않은 채 반환한다. 두 번째 return 앞에 src.Close 호출을 추가하면 해결할 수 있지만, 함수가 더 복잡하다면 문제를 알아차리고 바로잡기가 쉽지 않을 수 있다.
defer 문을 사용하면 파일을 항상 닫도록 보장할 수 있다.
func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil { return } defer src.Close() dst, err := os.Create(dstName) if err != nil { return } defer dst.Close() return io.Copy(dst, src) }
각 파일을 연 직후 닫는 작업을 defer로 지정하면, 함수에 반환 지점이 몇 개 있든 파일이 닫힌다.
defer 문의 동작에는 간단하고 예측 가능한 세 가지 규칙이 있다.
- 지연된 함수의 인수는
defer문이 평가될 때 평가된다. 다음 예제에서는Println호출을 지연하는 시점에i의 값이 평가된다. 따라서 함수가 반환된 뒤 지연된 호출은0을 출력한다.
func a() { i := 0 defer fmt.Println(i) i++ return }
- 지연된 함수 호출은 이를 감싼 함수가 반환된 뒤 후입선출(LIFO) 순서로 실행된다. 다음 함수는
3210을 출력한다.
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
- 지연된 함수는 반환하는 함수의 이름 있는 반환값을 읽고 수정할 수 있다. 다음 예제에서는 지연된 함수가 바깥 함수의 반환 이후 반환값
i를 증가시킨다. 따라서 이 함수는2를 반환한다.
func c() (i int) { defer func() { i++ }() return 1 }
이 기능은 함수의 오류 반환값을 수정할 때 유용하다. 잠시 뒤 예제를 살펴본다.
panic은 일반적인 제어 흐름을 멈추고 패닉을 시작하는 내장 함수다. 함수 F가 panic을 호출하면 F의 실행이 중단되고, F 안에 지연된 함수들이 정상적으로 실행된 뒤 F가 호출자에게 반환한다. 호출자 입장에서는 F가 panic을 호출한 것처럼 동작한다. 현재 고루틴의 모든 함수가 반환할 때까지 호출 스택을 따라 올라가며, 그 시점에 프로그램이 비정상 종료된다.
panic을 직접 호출해 패닉을 일으킬 수 있다. 배열 범위를 벗어난 접근 같은 런타임 오류도 패닉을 일으킬 수 있다.
recover는 패닉이 발생한 고루틴의 제어권을 되찾는 내장 함수다. recover는 지연된 함수 안에서만 유용하다. 정상 실행 중 호출하면 nil을 반환하고 다른 효과는 없다. 현재 고루틴에서 패닉이 발생한 상태라면 recover 호출은 panic에 전달된 값을 받아내고 정상 실행을 재개한다.
다음 프로그램은 panic과 defer의 동작을 보여준다.
package main import "fmt" func main() { f() fmt.Println("Returned normally from f.") } func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }
함수 g는 정수 i를 받아 i가 3보다 크면 패닉을 일으킨다. 그렇지 않으면 인수 i+1을 넣어 자기 자신을 호출한다. 함수 f는 recover를 호출하고, 반환값이 nil이 아니면 복구된 값을 출력하는 함수를 지연 실행하도록 등록한다.
계속 읽기 전에 프로그램의 출력이 어떨지 생각해 보자. 프로그램은 다음을 출력한다.
Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f.
f에서 지연된 함수를 제거하면 패닉은 복구되지 않고 고루틴의 호출 스택 맨 위까지 도달해 프로그램을 종료한다. 이렇게 수정한 프로그램은 다음을 출력한다.
Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 panic: 4 panic PC=0x2a9cd8 [stack trace omitted]
panic과 recover의 실제 사례로 Go 표준 라이브러리의 json 패키지를 볼 수 있다. 이 패키지는 재귀 함수를 이용해 인터페이스를 인코딩한다. 값을 순회하다 오류가 발생하면 panic을 호출해 호출 스택을 최상위 함수까지 되감는다. 최상위 함수는 패닉을 복구하고 적절한 오류값을 반환한다. 관련 내용은 encodeState 타입의 error 및 marshal 메서드가 있는 encode.go에서 확인할 수 있다.
Go 라이브러리에서는 패키지 내부에서 panic을 사용하더라도 외부 API는 명시적인 오류 반환값을 제공하는 것이 관례다.
파일을 닫는 예 외에도 defer는 뮤텍스 잠금을 해제하거나,
mu.Lock() defer mu.Unlock()
머리말을 출력한 뒤 꼬리말을 출력하는 데 쓸 수 있다.
printHeader() defer printFooter()
defer 문은 panic과 recover를 함께 쓰든 그렇지 않든, 제어 흐름을 다루는 독특하고 강력한 수단이다. 다른 프로그래밍 언어에서 특정 목적의 구조체로 구현하는 여러 기능을 모델링하는 데 쓸 수 있다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요