defer가 하는 일
defer는 함수 호출을 목록에 쌓습니다. 감싸는 함수가 반환되면 목록이 역순으로 실행됩니다.
출력:
start
end
deferred 3
deferred 2
deferred 1
순서는 스택처럼 후입선출입니다. 자원이 중첩되는 방식과 맞아떨어집니다. A를 연 다음 B를 열었다면 보통 A보다 B를 먼저 닫고 싶기 때문입니다.
획득 바로 옆에 정리 코드
defer의 주된 용도는 자원을 얻은 바로 다음 줄에 정리 코드를 써서, 어떤 반환 경로에서도 정리를 잊지 않게 하는 것입니다.
순서에 주목하세요. 오류를 먼저 확인하고, 그다음 defer합니다. os.Open이 실패했다면 f는 nil이고, 확인 전에 f.Close()를 defer하면 nil *os.File에 Close를 호출하게 됩니다(그 결과 보지도 못할 오류가 반환되고, 코드를 읽는 사람을 헷갈리게 합니다).
잠금에도 같은 형태가 통합니다:
mu.Lock()
defer mu.Unlock()
그 사이의 코드가 패닉을 일으켜도 뮤텍스는 해제됩니다.
인자는 즉시 평가된다
지연된 함수와 그 인자는 defer 문이 실행될 때 평가됩니다. 호출 자체만 기다립니다.
출력:
x is now 2
deferred closure reads: 2
deferred with argument: 1
fmt.Println("...", x)는 defer 줄에서 값 1을 캡처했습니다. 클로저는 인자가 없고, 마침내 실행될 때 x를 읽습니다. 기록하고 싶은 것에 맞는 형태를 고르세요.
메서드 리시버에도 적용됩니다. defer t.Stop()은 t를 즉시 평가하므로, 나중에 t에 다시 대입해도 멈추는 대상은 바뀌지 않습니다.
흔한 시간 측정 요령은 이 규칙을 일부러 활용합니다:
func handle() {
defer trace("handle")() // trace runs now, the returned func runs at exit
// ...
}
trace("handle")은 즉시 호출되고("enter"를 출력하고 시작 시간을 기록할 수 있습니다), 그것이 반환한 함수가 defer됩니다.
반복문 안의 defer
지연된 호출은 반복이 끝날 때마다가 아니라 함수가 반환될 때 실행됩니다. 많은 파일을 도는 반복문이라면 함수가 끝날 때까지 모든 파일이 열린 채로 남습니다.
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // all files stay open until the function returns
process(f)
}
경로가 수천 개라면 파일 디스크립터가 바닥납니다. 본문을 별도의 함수로 옮겨서 반복마다 defer가 실행되게 하세요:
processFile을 호출할 때마다 다음 파일이 열리기 전에 파일이 닫힙니다. 이름 있는 헬퍼가 과하다 싶으면 그 자리에서 호출하는 함수 리터럴(func() { ... }())도 똑같이 동작합니다.
반환 값 바꾸기
지연된 클로저는 return 문이 결과를 대입한 뒤에 실행되며, 호출자가 보기 전에 이름 있는 결과를 수정할 수 있습니다.
이 코드는 10과 save failed: disk full을 출력합니다. 결과에 이름이 없어도 지연된 함수는 실행되지만, 반환되는 값을 바꿀 방법은 없습니다.
Close의 오류 잡기
defer f.Close()는 Close의 오류를 버립니다. 읽기만 한 파일이라면 괜찮습니다. 쓰기를 한 파일이라면 Close가 마지막 플러시 실패를 보고할 수 있으므로 그 오류가 중요합니다. 이름 있는 결과를 쓰면 오류를 지킬 수 있습니다:
func writeReport(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
_, err = f.Write(data)
return err
}
두 오류를 모두 보고하고 싶다면 errors.Join(err, f.Close())가 더 짧은 대안입니다.
defer, panic, recover
지연된 호출은 패닉이 스택을 되감는 동안 실행됩니다. recover가 무언가를 하는 곳은 그곳뿐이며, 서버가 잘못된 요청 하나 때문에 프로세스 전체가 죽지 않게 하는 방법도 이것입니다. 자세한 내용은 panic과 recover에 있습니다.
프로그램이 os.Exit나 log.Fatal로 종료되면 지연된 호출은 실행되지 않습니다. main이 정리 작업을 defer한 뒤 os.Exit(1)을 호출하면 정리 작업은 건너뜁니다.
비용
Go 1.14부터 대부분의 defer는 컴파일러가 인라인 코드로 펼치며(open-coded), 비용은 몇 나노초입니다. 모든 뮤텍스 해제와 파일 닫기에 defer를 쓰는 것이 일반적인 스타일입니다. 반복문 안의 defer는 예외입니다. 이런 defer는 펼칠 수 없어서 더 느린 경로로 처리되며, 이것도 반복문 본문을 함수로 옮길 또 하나의 이유입니다.
흔한 실수
- 오류 확인 전에 defer함.
Open의err를 먼저 확인한 뒤defer Close를 쓰세요. - 반복문에서 반복마다 정리되길 기대함. defer는 함수가 끝날 때 실행됩니다.
- 지연된 인자가 이후의 변경을 볼 거라고 기대함. 인자는
defer줄에서 고정됩니다. 종료 시점에 읽으려면 클로저를 쓰세요. os.Exit와 함께 defer에 의존함. 절대 실행되지 않습니다.
자주 묻는 질문
Go에서 defer는 무엇을 하나요?
defer f()는 감싸는 함수가 반환될 때 f()가 실행되도록 예약합니다. 정상적으로 반환하든, 이른 return으로 반환하든, 패닉으로 끝나든 마찬가지입니다. 정리 작업(파일 닫기, 뮤텍스 해제)을 자원을 얻은 코드 바로 옆에 두는 데 씁니다.
Go에서 지연된 호출은 어떤 순서로 실행되나요?
후입선출(LIFO)입니다. 가장 최근에 defer한 호출이 먼저 실행됩니다. defer fmt.Println(1); defer fmt.Println(2)는 2를 출력한 다음 1을 출력합니다.
지연된 함수의 인자는 언제 평가되나요?
호출이 실행될 때가 아니라 defer 문이 실행되는 즉시 평가됩니다. x := 1; defer fmt.Println(x); x = 2는 1을 출력합니다. 종료 시점의 값을 읽으려면 클로저를 defer하세요: defer func() { fmt.Println(x) }().
defer는 패닉이나 os.Exit에서도 실행되나요?
지연된 호출은 패닉이 스택을 되감는 동안 실행되며, 그래서 그 안에서 recover가 동작합니다. 프로그램이 os.Exit(또는 이를 호출하는 log.Fatal)을 호출하면 실행되지 않고, main이 반환될 때 다른 고루틴의 지연된 호출도 실행되지 않습니다.