게시물에서 발췌한 내용입니다. 전체 게시물은 여기서 볼 수 있습니다: Golang Defer: From Basic To Trap.
defer 문은 우리가 Go를 배우기 시작할 때 가장 먼저 흥미롭다고 생각하는 것 중 하나일 것입니다. 그렇죠?
하지만 많은 사람들을 당황하게 만드는 것 외에도 우리가 사용할 때 종종 다루지 않는 흥미로운 측면이 많이 있습니다.
예를 들어 defer 문에는 실제로 3가지 유형이 있습니다(Go 1.22 기준, 나중에 변경될 수 있음): 개방형 지연, 힙 할당 지연 및 스택 할당. 각각은 성능이 다르고 가장 잘 사용되는 시나리오도 다르므로 성능을 최적화하려는 경우 알아두면 좋습니다.
이 토론에서는 기본부터 고급 사용법까지 모든 것을 다루며, 내부 세부 사항도 조금, 아주 조금만 파헤쳐 보겠습니다.
너무 깊이 들어가기 전에 지연에 대해 간단히 살펴보겠습니다.
Go에서 defer는 주변 함수가 완료될 때까지 함수 실행을 지연하는 데 사용되는 키워드입니다.
func main() { defer fmt.Println("hello") fmt.Println("world") } // Output: // world // hello
이 코드 조각에서 defer 문은 fmt.Println("hello")가 기본 함수의 맨 끝에서 실행되도록 예약합니다. 따라서 fmt.Println("world")가 즉시 호출되고 "world"가 먼저 인쇄됩니다. 이후에는 defer를 사용했기 때문에 메인이 끝나기 전 마지막 단계로 "hello"가 출력됩니다.
함수가 종료되기 직전에 나중에 실행할 작업을 설정하는 것과 같습니다. 이는 데이터베이스 연결 닫기, 뮤텍스 해제 또는 파일 닫기와 같은 정리 작업에 정말 유용합니다.
func doSomething() error { f, err := os.Open("phuong-secrets.txt") if err != nil { return err } defer f.Close() // ... }
위의 코드는 defer의 작동 방식을 보여주는 좋은 예이지만 defer를 사용하는 나쁜 방법이기도 합니다. 이에 대해서는 다음 섹션에서 다루겠습니다.
"좋습니다. 하지만 마지막에 f.Close()를 넣으면 어떨까요?"
여기에는 몇 가지 좋은 이유가 있습니다.
패닉이 발생하면 스택이 풀리고 지연된 함수가 특정 순서로 실행됩니다. 이에 대해서는 다음 섹션에서 다루겠습니다.
함수에서 여러 개의 defer 문을 사용하면 '스택' 순서로 실행됩니다. 즉, 마지막으로 지연된 함수가 먼저 실행됩니다.
func main() { defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) } // Output: // 3 // 2 // 1
defer 문을 호출할 때마다 다음과 같이 현재 고루틴의 연결 목록 맨 위에 해당 함수를 추가하게 됩니다.
그리고 함수가 반환되면 연결리스트를 순회하며 위 이미지의 순서대로 하나씩 실행하게 됩니다.
하지만 기억하세요. 고루틴의 연결 목록에서 모든 연기를 실행하는 것이 아니라 반환된 함수에서만 연기를 실행한다는 점을 기억하세요. 연기 연결 목록에는 다양한 함수의 많은 연기가 포함될 수 있기 때문입니다.
func B() { defer fmt.Println(1) defer fmt.Println(2) A() } func A() { defer fmt.Println(3) defer fmt.Println(4) }
따라서 현재 함수(또는 현재 스택 프레임)에서 지연된 함수만 실행됩니다.
그러나 현재 고루틴의 모든 지연된 기능이 추적되고 실행되는 일반적인 경우가 하나 있는데, 이때 패닉이 발생합니다.
컴파일 시간 오류 외에도 런타임 오류가 많이 있습니다. 0으로 나누기(정수만), 범위를 벗어남, nil 포인터 역참조 등이 있습니다. 이러한 오류로 인해 애플리케이션이 패닉 상태에 빠지게 됩니다.
패닉은 현재 고루틴의 실행을 중지하고, 스택을 풀고, 현재 고루틴에서 지연된 함수를 실행하여 애플리케이션이 충돌하도록 하는 방법입니다.
예기치 않은 오류를 처리하고 애플리케이션 충돌을 방지하려면 지연된 함수 내에서 복구 기능을 사용하여 패닉 상태의 고루틴에 대한 제어권을 다시 얻을 수 있습니다.
func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() panic("This is a panic") } // Output: // Recovered: This is a panic
보통 사람들은 패닉에 오류를 넣고 복구(..)를 사용하여 오류를 포착하지만 문자열, 정수 등 무엇이든 될 수 있습니다.
위의 예에서 deferred 함수 내부는 복구를 사용할 수 있는 유일한 장소입니다. 이에 대해 좀 더 설명하겠습니다.
여기에 나열할 수 있는 몇 가지 실수가 있습니다. 실제 코드에서 이와 같은 스니펫을 세 개 이상 본 적이 있습니다.
첫 번째는 복구를 지연된 함수로 직접 사용하는 것입니다.
func main() { defer recover() panic("This is a panic") }
위의 코드는 여전히 패닉 상태이며 이는 Go 런타임의 설계에 따른 것입니다.
복구 기능은 패닉을 포착하기 위한 것이지만 제대로 작동하려면 지연된 기능 내에서 호출되어야 합니다.
뒤에서 복구 호출은 실제로 Runtime.gorecover이며 복구 호출이 올바른 컨텍스트, 특히 패닉이 발생했을 때 활성화된 올바른 지연 함수에서 발생하는지 확인합니다.
"이렇게 지연된 함수 내부의 함수에서는 복구를 사용할 수 없다는 뜻인가요?"
func myRecover() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } } func main() { defer func() { myRecover() // ... }() panic("This is a panic") }
정확히 위의 코드는 예상한 대로 작동하지 않습니다. 그 이유는 복구가 지연된 함수에서 직접 호출되지 않고 중첩된 함수에서 호출되기 때문입니다.
이제 또 다른 실수는 다른 고루틴에서 패닉을 잡으려고 시도하는 것입니다.
func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() go panic("This is a panic") time.Sleep(1 * time.Second) // Wait for the goroutine to finish }
말이 되는군요, 그렇죠? 우리는 지연 체인이 특정 고루틴에 속한다는 것을 이미 알고 있습니다. 각 고루틴에는 자체 스택이 있으므로 하나의 고루틴이 다른 고루틴에 개입하여 패닉을 처리할 수 있다면 어려울 것입니다.
안타깝게도 이 경우 유일한 해결 방법은 해당 고루틴에서 패닉을 처리하지 않으면 애플리케이션이 충돌하는 것입니다.
이전에 오래된 데이터가 분석 시스템으로 푸시되는 문제가 발생했는데 그 이유를 파악하기가 어려웠습니다.
제 말은 다음과 같습니다.
func pushAnalytic(a int) { fmt.Println(a) } func main() { a := 10 defer pushAnalytic(a) a = 20 }
결과가 어떻게 될 것이라고 생각하시나요? 20이 아니라 10입니다.
defer 문을 사용하면 바로 값을 가져오기 때문입니다. 이를 "가치에 의한 포착"이라고 합니다. 따라서 나중에 a가 변경되더라도 연기가 예약되면 pushAnalytic으로 전송되는 a의 값은 10으로 설정됩니다.
이 문제를 해결하는 방법에는 두 가지가 있습니다.
...
전체 게시물은 여기서 볼 수 있습니다: Golang Defer: From Basic To Trap.
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3