🔥 defer, panic, recover

897자
10분

Go 언어에는 if, for, switch, goto 등 다양한 제어문이 있죠. 또한 별도의 고루틴에서 코드를 실행하기 위한 go 문도 있습니다. 이번에는 덜 일반적인 defer, panic, recover에 대해 알아볼까요?

defer로 함수 호출 미루기

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
}
 
go

이 코드는 작동하지만, 버그가 있어요. 만약 os.Create 호출이 실패하면, 이 함수는 소스 파일을 닫지 않고 리턴하게 됩니다. 두 번째 리턴문 전에 src.Close()를 호출하면 쉽게 해결할 수 있겠지만, 함수가 더 복잡해지면 이런 문제를 쉽게 발견하고 해결하기 어려울 수 있죠. defer 문을 도입하면 파일이 항상 닫히도록 보장할 수 있습니다:

func CopyFile(dstName, srcName string) (written int64, err error) {
    // 소스 파일 열기
    src, err := os.Open(srcName)
    if err != nil {
        return // 반환값에 err 로 이름이 있기 때문에 return err 라 쓰지 않고 return 만 적었다.
    }
    // 함수 리턴 전 소스 파일 닫기
    defer src.Close()
 
    // 대상 파일 생성
    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    // 함수 리턴 전 대상 파일 닫기
    defer dst.Close()
 
    // 소스에서 대상으로 파일 내용 복사하고 바이트 수 리턴
    return io.Copy(dst, src)
}
 
go

defer 문을 사용하면 각 파일을 연 직후 파일을 닫는 것에 대해 생각할 수 있어요. 함수 내 리턴문 개수에 관계없이 파일이 반드시 닫힌다는 것을 보장하죠.

defer 문의 동작은 간단하고 예측 가능합니다. 세 가지 간단한 규칙이 있어요:

  1. 지연된 함수의 인수는 defer 문이 평가될 때 평가됩니다.

이 예제에서 "i" 표현식은 Println 호출이 지연될 때 평가됩니다. 지연된 호출은 함수가 리턴된 후 "0"을 출력할 거예요.

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
 
go
  1. 지연된 함수 호출은 주변 함수가 리턴된 후 Last In First Out 순서로 실행됩니다.

이 함수는 "3210"을 출력해요:

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
 
go
  1. 지연된 함수는 리턴 함수의 명명된 리턴 값을 읽고 할당할 수 있습니다.

이 예제에서 지연된 함수는 주변 함수가 리턴된 후에 리턴 값 i를 증가시킵니다. 따라서 이 함수는 2를 리턴하죠:

func c() (i int) {
    defer func() { i++ }()
    return 1
}
 
go

이는 함수의 에러 리턴 값을 수정하는 데 편리하답니다. 이에 대한 예제를 곧 볼 거예요.

panic으로 비정상 종료하기

panic은 일반적인 제어 흐름을 멈추고 패닉 을 시작하는 내장 함수입니다. 함수 F가 panic을 호출하면, F의 실행이 중지되고, F에서 지연된 모든 함수가 정상적으로 실행된 다음, F가 호출자에게 리턴됩니다. 호출자에게 F는 panic 호출처럼 동작하게 되죠. 이 과정은 현재 고루틴의 모든 함수가 리턴될 때까지 계속되고, 그 시점에서 프로그램이 크래시됩니다. panic은 panic을 직접 호출하여 발생시킬 수 있어요. 또한 범위를 벗어난 배열 접근과 같은 런타임 에러로 인해 발생할 수도 있습니다.

recover로 패닉에서 복구하기

recover는 패닉 상태의 고루틴을 다시 정상 상태로 되돌리는 내장 함수예요. 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)
}
 
go

함수 g는 int i를 받아서, i가 3보다 크면 패닉을 일으키고, 그렇지 않으면 인수 i+1로 자신을 호출합니다. 함수 f는 recover를 호출하고 복구된 값을 출력하는 함수를 지연시키죠. 계속 읽기 전에 이 프로그램의 출력이 어떨지 생각해 보세요.

이 프로그램은 다음과 같이 출력될 거예요:

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.
text

만약 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]
text

panicrecover의 실제 예제를 보려면, Go 표준 라이브러리의 json 패키지를 확인해 보세요. 이 패키지는 재귀 함수 집합으로 인터페이스를 인코딩합니다. 값을 탐색하는 동안 에러가 발생하면, panic이 호출되어 스택을 최상위 함수 호출까지 풀어내고, 거기서 패닉에서 복구하여 적절한 에러 값을 리턴하죠(encode.go의 encodeState 타입의 'error'와 'marshal' 메서드 참조).

Go 라이브러리의 관례는 패키지가 내부적으로 panic을 사용하더라도, 외부 API는 여전히 명시적인 에러 리턴 값을 제공하는 거예요.

앞서 언급한 file.Close 예제 외에 defer의 다른 사용 예로는 뮤텍스 해제가 있습니다:

mu.Lock()
defer mu.Unlock()
 
go

푸터 출력하기:

printHeader()
defer printFooter()
 
go

등이 있죠.

요약하면, defer 문은(panic과 recover와 함께 또는 별도로) 제어 흐름을 위한 특이하고 강력한 메커니즘을 제공해요. 다른 프로그래밍 언어에서 특수 목적 구조로 구현된 여러 기능을 모델링하는 데 사용될 수 있습니다. 한번 시도해 보시길 바랍니다!