번역

[번역] [go] Defer, Panic, Recover 에 대해서

hayz 2022. 1. 27. 23:33
해당 글은 원문; https://go.dev/blog/defer-panic-and-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 예시이다.

  1. defer 함수의 변수는 선언 시점이 아닌 호출 시점에 평가된다. 
    - 아래 예시에서는 결과가 0 으로 나온다. 
    func a() {
     i := 0
     defer fmt.Println(i)
     i++
     return
    }
  2. defer 함수 들은 LIFO 방식으로 stack 과 같이 동작한다. 
    - 아래 예시에서는 결과가 3210 으로 나온다. 
    func b() {
     for i := 0; i < 4; i++ {
     defer fmt.Print(i)
     }
    }
  3. 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 를 포함하거나 않거나) 은 흐름제어에서 평범하지 않고 강력한 기능을 제공한다. 이는 특정한 목적의 구조에서 구현되어 사용될 수 있다. (다른 언어에서)