지연, 패닉 및 복구 함수를 통해 제어

완료됨

이제 Go에 고유한 제어 흐름(defer, panicrecover)을 살펴보겠습니다. 이러한 각 함수에는 여러 가지 사용 사례가 있습니다. 여기에서 가장 중요한 사용 사례를 살펴보겠습니다.

Defer 함수

Go에서 defer 문은 defer 문을 포함하는 함수가 완료될 때까지 함수(매개 변수 포함)의 실행을 연기합니다. 일반적으로 파일을 닫거나 정리 프로세스를 실행하는 등의 작업을 잊지 않아야 할 때 함수를 지연시킵니다.

원하는 만큼의 함수를 지연시킬 수 있습니다. defer 문은 마지막에서 첫 번째로, 즉 역순으로 실행됩니다.

다음 예제 코드를 실행하여 이 패턴이 제대로 작동하는지 확인합니다.

package main

import "fmt"

func main() {
    for i := 1; i <= 4; i++ {
        defer fmt.Println("deferred", -i)
        fmt.Println("regular", i)
    }
}

다음은 코드 출력입니다.

regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1

이 예제에서는 fmt.Println("deferred", -i)이 지연될 때마다 i의 값이 저장되고 그 함수 호출이 큐에 추가되었습니다. main() 함수에서 regular 값 출력을 완료한 후에 지연된 모든 호출이 실행되었습니다. 지연된 호출의 출력은 큐에서 도출된 역순(후입선출)입니다.

defer 함수의 일반적인 사용 사례는 사용을 마친 후 파일을 닫는 것입니다. 예를 들면 다음과 같습니다.

package main

import (
    "io"
    "os"
    "fmt"
)

func main() {
    newfile, error := os.Create("learnGo.txt")
    if error != nil {
        fmt.Println("Error: Could not create file.")
        return
    }
    defer newfile.Close()

    if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {
	    fmt.Println("Error: Could not write to file.")
        return
    }

    newfile.Sync()
}

파일을 만들거나 열고 작업을 마친 후 파일을 닫는 것을 잊지 않도록 .Close() 함수를 지연합니다.

Panic 함수

런타임 오류는 범위를 벗어난 인덱스를 사용하거나 nil 포인터를 추론하여 배열에 액세스하는 것과 같이 Go 프로그램을 패닉으로 만듭니다. 프로그램을 강제로 패닉으로 만들 수도 있습니다.

기본 제공 panic() 함수는 Go 프로그램에서 정상적인 제어 흐름을 중지합니다. panic 호출을 사용하면 지연된 함수 호출이 정상적으로 실행됩니다. 이 프로세스는 스택을 따라 모든 함수가 반환될 때까지 계속됩니다. 그러면 프로그램이 로그 메시지와 충돌합니다. 이 메시지에는 문제의 근본 원인을 진단하는 데 도움이 되는 오류 정보 및 스택 추적이 포함되어 있습니다.

panic() 함수를 호출할 때 어떤 값이든 인수로 추가할 수 있습니다. 일반적으로 panic 상태로 만드는 이유에 대한 오류 메시지를 보냅니다.

예를 들어 다음 코드는 panicdefer 함수를 결합합니다. 이 코드를 실행하여 제어 흐름이 중단되는 방식을 확인합니다. 정리 프로세스가 계속 실행됩니다.

package main

import "fmt"

func highlow(high int, low int) {
    if high < low {
        fmt.Println("Panic!")
        panic("highlow() low greater than high")
    }
    defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
    fmt.Println("Call: highlow(", high, ",", low, ")")

    highlow(high, low + 1)
}

func main() {
    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

출력은 다음과 같습니다.

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
panic: highlow() low greater than high

goroutine 1 [running]:
main.highlow(0x2, 0x3)
	/tmp/sandbox/prog.go:13 +0x34c
main.highlow(0x2, 0x2)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x1)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x0)
	/tmp/sandbox/prog.go:18 +0x298
main.main()
	/tmp/sandbox/prog.go:6 +0x37

Program exited: status 2.

코드가 실행되면 다음과 같은 현상이 일어납니다.

  1. 모든 것이 정상적으로 실행됩니다. 프로그램은 highlow() 함수에 전달된 높음 및 낮음 값을 출력합니다.

  2. low의 값이 high 값보다 크면 프로그램이 패닉 상태가 됩니다. Panic! 메시지가 표시됩니다. 이때 제어 흐름이 중단되고, 지연된 모든 함수가 Deferred... 메시지를 출력하기 시작합니다.

  3. 프로그램 작동이 중단되고 전체 스택 추적이 표시됩니다. Program finished successfully! 메시지가 표시되지 않습니다.

panic() 함수에 대한 호출은 일반적으로 심각한 오류가 예상되지 않을 때 실행됩니다. 프로그램 충돌을 방지하기 위해 recover()라는 다른 함수를 사용할 수 있습니다.

Recover 함수

프로그램 중단을 방지하고 내부적으로 오류를 보고해야 할 때가 있습니다. 아니면 프로그램 크래시 발생 전에 매우 복잡한 상태를 정리하고자 할 수 있습니다. 예를 들어 더 많은 문제를 방지하기 위해 리소스에 대한 모든 연결을 닫아야 할 수도 있습니다.

Go는 패닉 후에 제어력을 되찾을 수 있도록 기본 제공 recover() 함수를 제공합니다. recover를 호출하는 함수에서만 defer를 호출합니다. recover() 함수를 호출하면 nil을 반환하고 정상 실행에 영향을 주지 않습니다.

이전 코드의 main 함수를 수정하여 다음과 같이 recover() 함수에 대한 호출을 추가해 보세요.

func main() {
    defer func() {
	handler := recover()
        if handler != nil {
            fmt.Println("main(): recover", handler)
        }
    }()

    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

프로그램을 실행할 때 출력이 다음과 같아야 합니다.

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
main(): recover from panic highlow() low greater than high

Program exited.

이전 버전과의 차이가 보이나요? 주된 차이는 스택 추적 오류가 더 이상 표시되지 않는다는 것입니다.

main() 함수에서는 recover() 함수를 호출하는 익명 함수를 지연시킵니다. 프로그램이 panic 상태일 때 recover()를 호출하면 nil를 반환하지 못합니다. 여기에서 정리를 위해 무언가를 할 수는 있지만 이 예제에서는 무언가를 출력할 뿐입니다.

panicrecover 함수의 조합은 Go에서 예외를 처리하는 관용적인 방식입니다. 다른 프로그래밍 언어에서는 try/catch 블록을 사용합니다. Go에서는 여기서 살펴본 방법을 선호합니다.

자세한 내용은 Go에 기본 제공 try 함수를 추가하는 제안을 확인해 보세요.