[Go] defer
개요
defer는 함수가 종료되기 직전에 특정 함수 호출을 지연시키는 키워드입니다.
주요 특징:
- 지연 실행: 함수 반환 직전에 실행됨
- LIFO 순서: 스택 구조로 나중에 defer된 것이 먼저 실행
- 인자 즉시 평가: defer 문을 만나는 순간 인자가 평가됨
- 패닉 시에도 실행: panic 발생 시에도 defer는 실행됨
- 리소스 정리: 파일, 연결, 잠금 등의 정리에 필수적
- 반환값 수정 가능: Named return value를 수정할 수 있음
defer 기본 동작
1. 단순 실행 순서
package main
import "fmt"
func basicDefer() {
fmt.Println("Start")
defer fmt.Println("Deferred 1")
fmt.Println("Middle")
defer fmt.Println("Deferred 2")
fmt.Println("End")
}
func main() {
basicDefer()
}
// 출력:
// Start
// Middle
// End
// Deferred 2 ← 나중에 defer된 것이 먼저
// Deferred 1
2. LIFO (Last In, First Out) 순서
func lifoOrder() {
defer fmt.Println("First")
defer fmt.Println("Second")
defer fmt.Println("Third")
defer fmt.Println("Fourth")
fmt.Println("Function body")
}
func main() {
lifoOrder()
}
// 출력:
// Function body
// Fourth ← 마지막에 defer된 것부터
// Third
// Second
// First
3. 조건부 실행에서도 보장
func conditionalReturn(condition bool) {
defer fmt.Println("Cleanup always runs")
fmt.Println("Start")
if condition {
fmt.Println("Early return")
return
}
fmt.Println("Normal end")
}
func main() {
conditionalReturn(true)
fmt.Println("---")
conditionalReturn(false)
}
// 출력:
// Start
// Early return
// Cleanup always runs ← return해도 실행됨
// ---
// Start
// Normal end
// Cleanup always runs
인자 평가 시점
1. 인자는 즉시 평가됨
func argumentEvaluation() {
i := 0
defer fmt.Println("Deferred:", i) // i의 값(0)이 즉시 캡처됨
i++
fmt.Println("Current:", i)
}
func main() {
argumentEvaluation()
}
// 출력:
// Current: 1
// Deferred: 0 ← defer 시점의 값(0)이 출력됨
2. 포인터로 최신 값 참조
func pointerReference() {
i := 0
defer func() {
fmt.Println("Deferred:", i) // 클로저로 i 참조
}()
i++
fmt.Println("Current:", i)
}
func main() {
pointerReference()
}
// 출력:
// Current: 1
// Deferred: 1 ← 최신 값(1)이 출력됨
3. 함수 호출 vs 클로저
func printValue(msg string, val int) {
fmt.Printf("%s: %d\n", msg, val)
}
func evaluationComparison() {
x := 10
// 인자는 즉시 평가
defer printValue("Direct", x)
// 클로저는 실행 시점에 평가
defer func() {
printValue("Closure", x)
}()
x = 20
fmt.Println("Changed x to:", x)
}
func main() {
evaluationComparison()
}
// 출력:
// Changed x to: 20
// Closure: 20 ← 실행 시점의 값
// Direct: 10 ← defer 시점의 값
defer와 반환값
1. Named Return Value 수정
func modifyReturnValue() (result int) {
defer func() {
result++ // 반환값 수정 가능!
}()
return 5
}
func main() {
fmt.Println(modifyReturnValue()) // 6 (5가 아님!)
}
2. 반환값 로깅
func divide(a, b float64) (result float64, err error) {
defer func() {
if err != nil {
fmt.Printf("divide(%v, %v) failed: %v\n", a, b, err)
} else {
fmt.Printf("divide(%v, %v) = %v\n", a, b, result)
}
}()
if b == 0 {
err = fmt.Errorf("division by zero")
return
}
result = a / b
return
}
func main() {
divide(10, 2)
divide(10, 0)
}
// 출력:
// divide(10, 2) = 5
// divide(10, 0) failed: division by zero
3. 반환값 변환
func processData() (err error) {
defer func() {
if err != nil {
// 에러를 더 구체적으로 변환
err = fmt.Errorf("processData failed: %w", err)
}
}()
// 작업 수행
err = doSomething()
return
}
func doSomething() error {
return fmt.Errorf("something went wrong")
}
func main() {
if err := processData(); err != nil {
fmt.Println(err)
// processData failed: something went wrong
}
}
실전 활용 패턴
1. 파일 리소스 정리
import (
"bufio"
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // ✅ 파일 자동 닫기
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
return scanner.Err()
}
func writeFile(filename string, data []string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
writer := bufio.NewWriter(file)
defer writer.Flush() // ✅ 버퍼 플러시
for _, line := range data {
if _, err := writer.WriteString(line + "\n"); err != nil {
return err
}
}
return nil
}
2. Mutex 잠금 해제
import "sync"
type SafeCounter struct {
mu sync.Mutex
count int
}
func (sc *SafeCounter) Increment() {
sc.mu.Lock()
defer sc.mu.Unlock() // ✅ 자동 잠금 해제
sc.count++
}
func (sc *SafeCounter) Value() int {
sc.mu.Lock()
defer sc.mu.Unlock()
return sc.count
}
// RWMutex 예제
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock() // ✅ 읽기 잠금 해제
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock() // ✅ 쓰기 잠금 해제
c.data[key] = value
}
3. 데이터베이스 트랜잭션
import "database/sql"
func transferMoney(db *sql.DB, from, to int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback() // 에러 시 롤백
} else {
tx.Commit() // 성공 시 커밋
}
}()
// 출금
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
// 입금
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
if err != nil {
return err
}
return nil
}
4. HTTP 요청 본문 닫기
import (
"io"
"net/http"
)
func fetchURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close() // ✅ Body 자동 닫기
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
5. 시간 측정
import "time"
func measureTime(name string) func() {
start := time.Now()
return func() {
fmt.Printf("%s took %v\n", name, time.Since(start))
}
}
func slowFunction() {
defer measureTime("slowFunction")() // 함수 반환값을 defer
time.Sleep(100 * time.Millisecond)
// 작업 수행
}
// 또는 직접 구현
func anotherFunction() {
start := time.Now()
defer func() {
fmt.Printf("Execution time: %v\n", time.Since(start))
}()
time.Sleep(50 * time.Millisecond)
}
6. 패닉 복구
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic occurred: %v", r)
}
}()
result = a / b // b가 0이면 패닉
return
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("Error:", err)
// Error: panic occurred: runtime error: integer divide by zero
} else {
fmt.Println("Result:", result)
}
}
7. 상태 복원
func changeConfig(newValue string) {
oldValue := config
defer func() {
config = oldValue // ✅ 원래 상태 복원
}()
config = newValue
// 임시로 새 설정 사용
doSomethingWithNewConfig()
}
var config string
func doSomethingWithNewConfig() {
// 작업 수행
}
8. 로깅 및 추적
func trace(name string) func() {
fmt.Printf("Entering %s\n", name)
return func() {
fmt.Printf("Exiting %s\n", name)
}
}
func complexFunction() {
defer trace("complexFunction")()
fmt.Println("Doing work...")
nestedFunction()
}
func nestedFunction() {
defer trace("nestedFunction")()
fmt.Println("Nested work...")
}
func main() {
complexFunction()
}
// 출력:
// Entering complexFunction
// Doing work...
// Entering nestedFunction
// Nested work...
// Exiting nestedFunction
// Exiting complexFunction
9. 임시 디렉토리 정리
import (
"io/ioutil"
"os"
)
func processWithTempDir() error {
tmpDir, err := ioutil.TempDir("", "myapp-")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir) // ✅ 임시 디렉토리 삭제
// tmpDir 사용
fmt.Println("Using temp dir:", tmpDir)
return nil
}
10. 연결 풀 반환
type Connection struct {
id int
}
type Pool struct {
conns chan *Connection
}
func (p *Pool) Get() *Connection {
return <-p.conns
}
func (p *Pool) Put(conn *Connection) {
p.conns <- conn
}
func useConnection(pool *Pool) error {
conn := pool.Get()
defer pool.Put(conn) // ✅ 연결 반환 보장
// 연결 사용
fmt.Printf("Using connection %d\n", conn.id)
return nil
}
defer와 panic/recover
func recoverExample() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
fmt.Println("About to panic")
panic("something went wrong")
fmt.Println("This won't be printed")
}
func main() {
recoverExample()
fmt.Println("Program continues")
}
// 출력:
// About to panic
// Recovered from panic: something went wrong
// Program continues
중첩 defer와 panic
func nestedDefer() {
defer fmt.Println("Defer 1")
defer fmt.Println("Defer 2")
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
defer fmt.Println("Defer 3")
panic("error!")
}
func main() {
nestedDefer()
}
// 출력:
// Defer 3
// Recovered: error!
// Defer 2
// Defer 1
defer 루프에서 사용
1. 잘못된 사용 (메모리 누수)
func badLoopDefer() {
for i := 0; i < 5; i++ {
file, _ := os.Open(fmt.Sprintf("file%d.txt", i))
defer file.Close() // ❌ 함수 종료 시까지 모두 열려있음!
}
// 5개 파일이 모두 열린 채로 유지됨
}
2. 올바른 사용 (즉시 정리)
func goodLoopDefer() {
for i := 0; i < 5; i++ {
func() {
file, _ := os.Open(fmt.Sprintf("file%d.txt", i))
defer file.Close() // ✅ 익명 함수 종료 시 닫힘
// 파일 처리
}()
}
}
// 또는 별도 함수로 분리
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
// 파일 처리
return nil
}
func goodLoopDefer2() {
for i := 0; i < 5; i++ {
processFile(fmt.Sprintf("file%d.txt", i))
}
}
성능 고려사항
1. defer 오버헤드
import "testing"
func BenchmarkWithoutDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
increment()
}
}
func BenchmarkWithDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
incrementWithDefer()
}
}
var counter int
func increment() {
counter++
}
func incrementWithDefer() {
defer func() {}()
counter++
}
// defer는 약간의 오버헤드 있음 (나노초 단위)
// 대부분의 경우 무시 가능
// 리소스 정리의 안전성이 더 중요
2. 핫 패스에서의 defer
// ❌ 성능 중요 루프에서 defer
func badHotPath(data []int) int {
sum := 0
for _, v := range data {
func() {
defer func() {}() // 불필요한 오버헤드
sum += v
}()
}
return sum
}
// ✅ 루프 밖에서 defer
func goodHotPath(data []int) (sum int) {
defer func() {
// 필요한 정리 작업
}()
for _, v := range data {
sum += v
}
return
}
일반적인 실수
1. 루프에서 defer 남용
func mistake1() {
// ❌ 모든 파일이 함수 종료까지 열려있음
for i := 0; i < 100; i++ {
f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
defer f.Close()
}
}
func fixed1() {
// ✅ 각 반복마다 파일 닫힘
for i := 0; i < 100; i++ {
func(i int) {
f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
defer f.Close()
// 파일 처리
}(i)
}
}
2. defer 인자 평가 시점 오해
func mistake2() {
i := 0
defer fmt.Println(i) // i의 값 0이 즉시 캡처됨
i = 10
// "10"이 아닌 "0"이 출력됨
}
func fixed2() {
i := 0
defer func() {
fmt.Println(i) // 클로저로 최신 값 참조
}()
i = 10
// "10"이 출력됨
}
3. 에러 무시
func mistake3(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // ❌ Close 에러 무시
// 파일 처리
return nil
}
func fixed3(filename string) (err error) {
file, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
if cerr := file.Close(); cerr != nil && err == nil {
err = cerr // ✅ Close 에러 처리
}
}()
// 파일 처리
return nil
}
4. nil 포인터 defer
func mistake4(shouldOpen bool) error {
var file *os.File
var err error
if shouldOpen {
file, err = os.Open("test.txt")
if err != nil {
return err
}
}
defer file.Close() // ❌ shouldOpen이 false면 nil.Close() 패닉!
return nil
}
func fixed4(shouldOpen bool) error {
var file *os.File
var err error
if shouldOpen {
file, err = os.Open("test.txt")
if err != nil {
return err
}
defer file.Close() // ✅ 파일이 열렸을 때만 defer
}
return nil
}
5. defer로 변수 섀도잉
func mistake5() (err error) {
defer func() {
if err != nil {
// err 처리
}
}()
// ❌ 새로운 err 변수 생성 (섀도잉)
file, err := os.Open("test.txt")
if err != nil {
return err // defer에서 캡처한 err는 여전히 nil
}
defer file.Close()
return nil
}
func fixed5() (err error) {
defer func() {
if err != nil {
// err 처리
}
}()
// ✅ 기존 err 재사용
var file *os.File
file, err = os.Open("test.txt")
if err != nil {
return err
}
defer file.Close()
return nil
}
6. recover 위치 오류
func mistake6() {
// ❌ recover가 defer 안에 없으면 작동 안 함
recover()
panic("error")
}
func fixed6() {
// ✅ defer 안에서 recover 호출
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("error")
}
고급 패턴
1. 조건부 defer
func conditionalDefer(cleanup bool) {
if cleanup {
defer fmt.Println("Cleanup performed")
}
fmt.Println("Function body")
}
2. defer 스택 추적
import "runtime"
func stackTrace() {
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 1024)
n := runtime.Stack(buf, false)
fmt.Printf("Panic: %v\n%s\n", r, buf[:n])
}
}()
causePanic()
}
func causePanic() {
panic("something went wrong")
}
3. 메트릭 수집
type Metrics struct {
mu sync.Mutex
requestCount int
totalDuration time.Duration
}
func (m *Metrics) recordRequest() func() {
m.mu.Lock()
m.requestCount++
m.mu.Unlock()
start := time.Now()
return func() {
duration := time.Since(start)
m.mu.Lock()
m.totalDuration += duration
m.mu.Unlock()
}
}
func handleRequest(m *Metrics) {
defer m.recordRequest()()
// 요청 처리
time.Sleep(10 * time.Millisecond)
}
4. 리소스 풀 패턴
type ResourcePool struct {
resources chan Resource
}
type Resource struct {
id int
}
func (p *ResourcePool) Acquire() (*Resource, func()) {
r := <-p.resources
return r, func() {
p.resources <- r
}
}
func useResource(pool *ResourcePool) {
resource, release := pool.Acquire()
defer release()
// 리소스 사용
fmt.Printf("Using resource %d\n", resource.id)
}
5. 트랜잭션 래퍼
type Transaction struct {
committed bool
}
func (t *Transaction) Commit() {
t.committed = true
}
func (t *Transaction) Rollback() {
// 롤백 로직
}
func beginTransaction() (*Transaction, func()) {
tx := &Transaction{}
return tx, func() {
if !tx.committed {
tx.Rollback()
}
}
}
func doWork() error {
tx, cleanup := beginTransaction()
defer cleanup()
// 작업 수행
if err := performWork(); err != nil {
return err
}
tx.Commit()
return nil
}
func performWork() error {
return nil
}
defer vs 명시적 정리
// defer 사용 (권장)
func withDefer() error {
file, err := os.Open("test.txt")
if err != nil {
return err
}
defer file.Close()
// 여러 반환 경로가 있어도 안전
if someCondition {
return nil
}
if anotherCondition {
return fmt.Errorf("error")
}
return nil
}
// 명시적 정리 (유지보수 어려움)
func withoutDefer() error {
file, err := os.Open("test.txt")
if err != nil {
return err
}
if someCondition {
file.Close() // 모든 경로에서 닫아야 함
return nil
}
if anotherCondition {
file.Close() // 빠뜨리기 쉬움
return fmt.Errorf("error")
}
file.Close()
return nil
}
var someCondition = false
var anotherCondition = false
정리
- defer: 함수 종료 직전에 실행 보장
- LIFO 순서: 나중에 defer된 것이 먼저 실행 (스택)
- 인자 즉시 평가: defer 문을 만날 때 인자가 평가됨
- 클로저로 최신 값: 익명 함수로 실행 시점 값 참조
- Named return value: defer에서 반환값 수정 가능
- 리소스 정리: 파일, 연결, 잠금 해제에 필수
- Panic에도 실행: panic 발생 시에도 defer는 실행됨
- 루프 주의: 루프에서는 익명 함수로 감싸기
- 성능: 약간의 오버헤드 있지만 대부분 무시 가능
- 에러 처리: Close 등의 에러도 적절히 처리
- nil 체크: defer 전에 nil 체크 필수
- recover: defer 안에서만 작동
- 가독성과 안전성: 명시적 정리보다 defer가 더 안전