Go defer: 인자는 언제 평가되고, 어떤 순서로 실행되며, 반환값은 어떻게 바뀌나

Go의 defer는 인자를 defer 문 실행 시점에 평가하고, 호출은 함수가 반환되기 직전에 역순으로 실행합니다. 이름 있는 결과 매개변수를 바꿀 수 있는 이유까지 언어 명세로 정리합니다.

godeferlanguage-specclosures

Go에서 defer f(x)를 쓰면 f는 나중에 호출됩니다. 그런데 x는 나중이 아니라 지금 평가됩니다. 이 차이 때문에 로그에 예상과 다른 값이 찍히거나, return 3을 했는데 호출한 쪽에서 30을 받는 일이 생깁니다. 이 글은 Go 언어 명세에 적힌 규칙만으로 이 동작을 설명합니다.

규칙 정리

Go 명세의 “Defer statements” 절이 정한 내용은 다음과 같습니다.

  • defer 뒤에는 함수 호출이나 메서드 호출이 와야 하고, 괄호로 감쌀 수 없습니다.
  • defer 문이 실행되는 순간 함수 값과 인자가 평가되어 저장됩니다. 함수 자체는 이때 호출되지 않습니다.
  • 저장된 호출은 바깥 함수가 반환되기 직전에 실행됩니다. 반환 경로는 return 문 실행, 함수 본문 끝 도달, 해당 고루틴의 패닉 중 어느 것이든 해당합니다.
  • 여러 개를 defer하면 등록한 순서의 역순으로 실행됩니다.
  • 명시적 return으로 반환할 때는 return이 결과 매개변수에 값을 먼저 넣고, 그다음 defer된 함수들이 실행되고, 마지막으로 호출한 쪽으로 돌아갑니다.

Effective Go는 메서드의 경우 리시버도 인자와 함께 defer 시점에 평가된다고 덧붙입니다. 또 defer는 블록 단위가 아니라 함수 단위로 동작합니다. if 블록 안에서 defer해도 그 블록이 끝날 때가 아니라 바깥 함수가 반환될 때 실행됩니다.

예제

아래 코드는 세 가지 규칙을 한 번에 보여 줍니다.

package main

import "fmt"

func show(label string, v int) {
	fmt.Println(label, v)
}

func compute() (n int) {
	x := 1
	defer show("argument:", x)
	defer func() { show("closure:", x) }()
	defer func() { n *= 10 }()
	x = 2
	return 3
}

func main() {
	fmt.Println("result:", compute())
}

명세에 따라 도출한 출력은 다음과 같습니다. 이 환경에는 Go 툴체인이 없어서 실제로 실행한 결과가 아닙니다.

closure: 2
argument: 1
result: 30

한 줄씩 따라가 보면 이렇습니다.

  1. 첫 번째 defer show("argument:", x)가 실행될 때 x는 1입니다. 이 1이 인자로 저장됩니다.
  2. 두 번째 defer는 인자가 없는 함수 리터럴을 저장합니다. 함수 리터럴은 클로저라서 바깥 함수의 변수 x를 복사하지 않고 공유합니다. 따라서 이 클로저가 나중에 실행될 때 읽는 값은 그 시점의 x입니다.
  3. 세 번째 defer는 결과 매개변수 n을 10배로 만드는 클로저를 저장합니다.
  4. x = 2가 실행됩니다.
  5. return 3이 n에 3을 넣습니다.
  6. defer된 호출이 역순으로 실행됩니다. 먼저 n이 30이 되고, 다음으로 클로저가 현재 x인 2를 출력하고, 마지막으로 저장해 둔 인자 1로 show가 호출됩니다.
  7. compute는 n의 값 30을 반환합니다.

argument: 1과 closure: 2의 차이가 핵심입니다. 인자는 defer 시점에 평가된 값이 저장되므로 이 예제에서는 정수 1이 쓰이고, 클로저 안에서 읽는 변수는 실행 시점의 값을 봅니다. 다만 인자를 저장하는 것은 깊은 복사가 아닙니다. 포인터, 슬라이스, 맵처럼 다른 데이터를 참조하는 값을 넘기면 저장되는 것은 그 참조 값이므로, 참조 대상이 나중에 바뀌면 defer된 호출에서도 바뀐 내용이 보일 수 있습니다.

이름 있는 결과 매개변수를 바꿀 수 있는 이유

명세의 “Return statements” 절에 따르면 결과 매개변수에 이름을 붙이면 그것은 일반 지역 변수처럼 동작하고, 함수 진입 시 해당 타입의 제로 값으로 초기화됩니다. 그리고 결과를 지정한 return 문은 defer된 함수가 실행되기 전에 결과 매개변수를 설정합니다.

그래서 defer된 함수 리터럴이 그 결과 매개변수를 볼 수 있는 위치에 있으면, 이미 설정된 반환값을 읽거나 바꿀 수 있습니다. 명세에도 return 6 뒤에 defer된 클로저가 result *= 7을 실행해 42를 반환하는 예제가 있습니다. 같은 원리로, 결과 매개변수 err에 이름을 붙여 두면 defer된 클로저에서 err를 검사하고 다른 값으로 바꿀 수도 있습니다.

반대로 결과에 이름이 없으면 defer된 함수가 접근할 변수 이름이 없으므로 이런 방식으로 반환값을 바꿀 수 없습니다. 또 defer된 함수 자신이 값을 반환하더라도 그 값은 버려집니다. defer된 함수의 반환값으로 바깥 함수의 결과를 바꾸는 방법은 없습니다.

자주 놓치는 점

  • nil 함수 값: defer된 함수 값이 nil이면 defer 문에서가 아니라 나중에 그 함수를 호출하려는 시점에 패닉이 발생합니다.
  • 반복문 안의 defer: 반복마다 호출이 하나씩 쌓이고, 반복문이 끝날 때가 아니라 함수가 반환될 때 한꺼번에 역순으로 실행됩니다. 명세 예제 for i := 0; i <= 3; i++ { defer fmt.Print(i) }는 3, 2, 1, 0 순서로 출력합니다.
  • 인자 평가 시점: 나중 값을 원하면 인자로 넘기지 말고 클로저 안에서 변수를 읽어야 합니다. 지금 평가한 값을 쓰고 싶다면 인자로 넘기면 됩니다. 단, 포인터나 슬라이스, 맵을 넘기면 참조 대상의 사본이 만들어지지는 않습니다.

범위와 출처

이 글은 2026-10-13에 확인한 Go 언어 명세(페이지 표기: Language version go1.27, May 26, 2026)를 기준으로 합니다. 각 규칙이 어느 버전부터 명세에 있었는지는 다루지 않습니다. recover, defer의 성능, 컴파일러 최적화 같은 구현 세부 사항도 다루지 않습니다. 예제 출력은 명세 규칙으로 도출한 것이며 실행 결과가 아닙니다.

아래 링크는 2026-10-13에 확인했습니다.

블로그로