글 작성자: cjwoov
반응형

2줄 요약

  • recover 는 defer함수에 의해 직접적으로 호출되면 안된다.
  • panic을 중단시킬 함수의 depth가 n이라면 recover는 n+1 에서 호출되어야 한다.

착각하기 쉬운 케이스

다른 언어의 try-catch문과 비슷하게 go에서는 defer, panic, recover 패턴을 이용해서 발생하는 예외 상황을 제어 할 수 있다.

 

package main

import (
	"fmt"
)

func main() {
	defer a()
	
	panic("panic!")
	
}

func a() {
	r := recover()
	fmt.Println("recoverd: ", r)
	fmt.Println("a called")
}
recoverd:  panic!
a called

Program exited.

만약 지연 호출 시킬 함수가 recover외에 하나 더 있다면 다음과 같이 작성 할 수도 있다.

package main

import (
	"fmt"
)

func main() {
	defer b()
	defer a()
	
	panic("panic!")
	
}

func a() {
	r := recover()
	fmt.Println("recoverd: ", r)
	fmt.Println("a called")
}

func b() {
	fmt.Println("b called")
}
recoverd:  panic!
a called
b called

Program exited.

 

여기서 더 나아가 코드를 좀 더 깔끔하게 정돈하고 싶어서 익명 함수로 a()와 b()를 묶는 경우가 있는데

이 경우가, 우리가 범하기 쉬운 실수다.

package main

import (
	"fmt"
)

func main() {
	defer func() {
		a()
		b()
	}()
	
	panic("panic!")
	
}

func a() {
	r := recover()
	fmt.Println("recoverd: ", r)
	fmt.Println("a called")
}

func b() {
	fmt.Println("b called")
}
recoverd:  <nil>
a called
b called
panic: panic!

goroutine 1 [running]:
main.main()
	/tmp/sandbox866461673/main.go:13 +0x60

Program exited: status 2.

생각대로라면 당연히 a함수에서 recover를 하고있어 panic을 막을 수 있을 것만 같지만 아니다.

익명 함수 호출 -> a 호출(recover) 과 같이 함수를 2번 타기 때문에 depth가 달라져 panic을 막지 못한다.

 

다음과 같은 경우도 panic을 처리하지 못한다.

package main

func main() {
	defer recover()
	panic("panic!")
}
panic: panic!

goroutine 1 [running]:
main.main()
	/tmp/sandbox609508284/main.go:5 +0x60

Program exited: status 2.

 

Go 오피셜에서는 다음과 같이 말하고 있다.

The return value of recover is nil if any of the following conditions holds:

  • panic's argument was nil;
  • the goroutine is not panicking;
  • recover was not called directly by a deferred function.

recover was not called directly by a deferred function. 

즉, recover는 지연된 함수에 의해 직접적으로 호출되면 안된다는 뜻이다.

 

여기서 recover는 panic을 중단 시킬 함수의 depth가 n이라면 recover는 n+1에서 호출해야 panic을 막을 수 있다는 것을 유추 할 수 있는데 좀 더 깊게 파들어가 보자.

 


Panic이 여러 번 호출된다면?

만약에 panic 1이 main함수 내에서 발생하였고 defer문에서 panic 2가 또 발생 했다면 recover를 하면 panic 1이 잡힐 까? panic 2가 잡힐까?

package main

import "fmt"

func main() {
	defer fmt.Println("program will not crash")

	defer func() {
		fmt.Println( recover() ) // 3
	}()

	defer fmt.Println("now, panic 3 suppresses panic 2")
	defer panic(3)
	defer fmt.Println("now, panic 2 suppresses panic 1")
	defer panic(2)
	panic(1)
}
now, panic 2 suppresses panic 1
now, panic 3 suppresses panic 2
3
program will not crash

defer문은 역방향으로 동작하기 때문에 가장 나중에 지연되어 호출된 panic(3)이 recover에 잡히게 된다.

 

그렇다면, 만약 panic이 발생하는 depth가 서로 다르다면..?

package main

import "fmt"

func main() { // call depth 0
	defer fmt.Println("to crash, for panic 3 is still active")

	defer func() { // call depth 1
		defer func() { // call depth 2
			fmt.Println( recover() ) // 6
		}()

		// The depth of panic 3 is 0,
		// and the depth of panic 6 is 1.
		defer fmt.Println("now, two active panics: 3 and 6")
		defer panic(6) // will suppress panic 5
		defer panic(5) // will suppress panic 4

		// The following panic will not suppress
		// panic 3,  for they have different depths.
		// The depth of panic 3 is 0.
		// The depth of panic 4 is 1.
		panic(4)
	}()

	defer fmt.Println("now, only panic 3 is active")
	defer panic(3) // will suppress panic 2
	defer panic(2) // will suppress panic 1
	panic(1)
}
now, only panic 3 is active
now, there are two active panics: 3 and 6
6
program will crash, for panic 3 is still active
panic: 1
	panic: 2
	panic: 3

goroutine 1 [running]
...

위의 결과를 분석하면 panic(1) -> panic(2) -> panic(3) -> panic(4) -> panic(5) -> panic(6) 순으로  패닉이 발생하였고

1, 2, 3depth 0 그리고 4, 5, 6depth 1에서 발생하였다.

depth 1에서 가장 나중에 발생한 panic(6)4와 5를 덮어 씌웠고 depth 2recover에 의해 복구되었다.

depth 2recover는 depth 1의 panic(6)을 복구한 것이지 depth 0의 panic에는 영향을 미치지 않기 때문에 panic(3)를 처리하지 못하고 프로그램은 종료된다.


결론

recover 호출이 적용되는 규칙은 간단하게 다음과 같다.

함수 f의 depth가 n이고, f에서 panic이 발생하여 복구하고 싶다면 recover는 지연 호출되어야 하고 depth는 n+1이 되어야 한다.

 

인용: https://go101.org/article/panic-and-recover-more.html
 

The Right Places to Call the recover Function - Go 101 (Golang Knowledgebase)

Panic and recover mechanism has been introduced before, and several panic/recover use cases are shown in the last article. We know that a recover call can only take effect if it is invoked in a deferred function call. However, not all recover calls in defe

go101.org

반응형