[번역] [go] Defer, Panic, Recover 에 대해서
go 는 if, for, switch, goto 와 같은 흐름 제어에 유용한 매커니즘을 가지고 있다. 또한 별도의 goroutine 에서 코드를 실행하기 위한 go statement 도 가지고 있다. 여기에서는 defer, panic, recover 라는 몇개의 명령어 들에 대해서 살펴본다.
defer
defer 는 함수 실행을 리스트에 넣어둔다. 이 리스트에 저장된 함수 실행들은 전체 함수가 모두 종료되면 실행된다. defer 함수는 보통 변수들에 대한 clean-up 동작을 수행하기 위해 실행된다.
예를 들어, 2개의 파일을 열고, 복사하는 동작을 하는 함수를 살펴본다.
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 함수가 실패하게 되면, 위에 os.Open 으로 받은 파일에 대한 자원을 반납하지 않고 return 되기 때문이다. 위 방식을 간단하게 해결하기 위해서는 src.Close 를 os.Create 되기 전에 선언하면 될거 같지만, 이 방법은 꽤나 흐름을 복잡하게 가져가는 문제가 있다. (os.Create 작업 이전에 반납되면 안되기 때문) 이를 위해 defer 를 이용하여 다음과 같이 작성할 수 있다. (+아래 코드에서는 src.Close가 defer 로 선언되어 있기 때문에 그 다음 os.Create 구문에서 에러가 나더라도, 탈출 전에 자원을 반납하게 된다. )
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 는 개발자로 하여금 해당 함수에 몇개의 return 이 존재 한다 해도 각 파일을 오픈 뒤에 제대로 닫는 것을 보장하게 된다.
defer 구문은 정직하고 예측이 가능하다. 아래는 세가지 defer 예시이다.
- defer 함수의 변수는 선언 시점이 아닌 호출 시점에 평가된다.
- 아래 예시에서는 결과가 0 으로 나온다.
func a() {
i := 0
defer fmt.Println(i)
i++
return
} - defer 함수 들은 LIFO 방식으로 stack 과 같이 동작한다.
- 아래 예시에서는 결과가 3210 으로 나온다.
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
} - defer 함수 들은 반환 값을 읽고 재 할당 할 수 있다.
- 아래 예시에서 반환 값은 2 이다.
func c() (i int) {
defer func() { i++ }()
return 1
}
(+위 결과들로 봤을 때, defer 함수는 return 값이 정해지면 동작하지만, 실제 함수가 호출된 위치로 복귀하였을 때는 defer 로 호출한 함수 부터 동작을 실행한다. )
이는 함수의 에러 반환 값을 수정하기에 용이하다. 이에 대한 예시를 간단하게 보겠다.
panic 은 일반적인 흐름을 멈추고, panicking 을 시작하는 빌트인 함수이다. 함수 f 가 panic 을 불렀을 때, f 의 동작은 멈추고, f 에 존재하는 모든 defer 함수들이 실행된다. 그다음에 f 가 다시 호출된 부분으로 반환된다. 호출을 한 함수에 f 는 panic 을 호출 하였다 알려준다. 프로세스는 현재 goroutine 에 존재하는 모든 함수들이 끝날때까지 계속 진행하며, 이 때 프로그램이 충돌된다. panic 은 직접 호출하여 panic 을 시작할 수 있다. 이는 런타임 에러이다.
recover 은 panicking goroutine 의 흐름을 다시 시작하는 빌트인 함수이다. recover는 defer 함수 안에서만 사용된다. 보통의 실행 환경 중에, recover 실행은 nil 을 반환하고 어떤 영향도 주지 않도록 한다. 만약 현재 goroutine 이 패닉상태라면, 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보다 크면 panic 을 호출하고, 그게 아니라면 재귀 함수로 i+1 을 인자로 넣어 실행한다. 함수 f 는 recover 하는 함수와 recover 결과 값을 출력하는 함수를 defer 로 설정한다.
위 실행에 대한 결과는 아래와 같다.
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 함수에서 defer 함수를 제거하면, panic 은 회복되지 않고 goroutine 의 가장 상위 stack 까지 올라가게 되며 프로그램이 종료된다. defer 함수를 제거했을때 출력 예시이다.
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
실제 환경에서의 panic 과 recover 예시는 go 표준 라이브러리의 json package 에서 볼 수 있다. 여기서는 재귀 함수로 인터페이스를 인코딩 하는 부분이 들어가 있는데, 여기에서 만약 에러가 발생한다면 panic 이 호출되고 여기서 error 값을 잡아 recover 를 진행하게 된다.
예시 : https://go.dev/src/pkg/encoding/json/encode.go
go 라이브러리의 컨벤션은 패키지가 내부적으로 panic 을 사용하더라도, 외부 API 는 여전히 error 반환 값을 표시한다는 것이다.
defer 의 다른 사용은 아래와 같다.
mu.Lock()
defer mu.Unlock()
printHeader()
defer printFooter()
요약하면, defer (panic 이나 recover 를 포함하거나 않거나) 은 흐름제어에서 평범하지 않고 강력한 기능을 제공한다. 이는 특정한 목적의 구조에서 구현되어 사용될 수 있다. (다른 언어에서)