기술 자료
Go 프로그램은 os.Exit
또는 log.Fatal*
을 사용하여 즉시 종료합니다 (panic
을 사용하는 것은 프로그램을 종료하는 좋지 않은 방법이므로 사용하지 마십시오).
main()
함수에서만 os.Exit
또는 log.Fatal*
을 호출하십시오. 다른 모든 함수는 호출자에게 오류를 반환해야 합니다.
비권장 사항:
func main() {
body := readFile(path)
fmt.Println(body)
}
func readFile(path string) string {
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
b, err := os.ReadAll(f)
if err != nil {
log.Fatal(err)
}
return string(b)
}
권장 사항:
func main() {
body, err := readFile(path)
if err != nil {
log.Fatal(err)
}
fmt.Println(body)
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
b, err := os.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
}
일반적으로 여러 종료 지점이 있는 프로그램은 여러 문제를 가집니다:
- 불분명한 제어 흐름: 어떤 함수든지 프로그램을 종료할 수 있으므로 제어 흐름에 대한 추론이 어려워집니다.
- 테스트하기 어려움: 프로그램을 종료하는 함수는 호출하는 테스트도 종료시킵니다. 이로 인해 함수를 테스트하기 어려워지며
go test
로 실행되지 않은 다른 테스트를 건너뛸 위험이 있습니다. - 정리 뛰어넘기: 함수가 프로그램을 종료하면 지연된 함수 호출을 뛰어넘습니다. 이로 인해 중요한 정리 작업을 건너뛸 위험이 커집니다.
한 번의 종료
가능하다면 main()
함수에서는 os.Exit
또는 log.Fatal
호출이 최대 한 번만 되어야 합니다. 프로그램 실행을 중지해야 하는 여러 오류 시나리오가 있는 경우 해당 논리를 별도의 함수에 배치하고 해당 함수로부터 오류를 반환하십니다. 이렇게 하면 main()
함수가 짧아지고 모든 중요한 비즈니스 로직이 별도의 테스트 가능한 함수에 위치하게 됩니다.
비권장 접근 방식:
package main
func main() {
args := os.Args[1:]
if len(args) != 1 {
log.Fatal("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 이 줄 이후에 log.Fatal을 호출하는 경우
// f.Close가 실행됩니다.
b, err := os.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
}
권장 접근 방식:
package main
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
args := os.Args[1:]
if len(args) != 1 {
return errors.New("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
b, err := os.ReadAll(f)
if err != nil {
return err
}
// ...
}
위 예제는 log.Fatal
을 사용하지만 이 지침은 os.Exit
또는 os.Exit
를 호출하는 라이브러리 코드에도 적용됩니다.
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
필요에 따라 run()
의 서명을 변경할 수 있습니다. 예를 들어, 프로그램이 특정 실패 코드로 종료해야 하는 경우 run()
은 오류 대신 종료 코드를 반환할 수 있습니다. 이는 또한 단위 테스트가 이 동작을 직접적으로 검증할 수 있도록 합니다.
func main() {
os.Exit(run(args))
}
func run() (exitCode int) {
// ...
}
위 예제에서 사용된 run()
함수는 필수적인 것은 아닙니다. run()
함수의 이름, 서명 및 설정은 유연합니다. 다른 것들 중에서 다음과 같은 작업을 할 수 있습니다:
- 구문 분석되지 않은 명령줄 인수 수락 (예:
run(os.Args[1:])
) -
main()
에서 명령줄 인수를 구문 분석하고run
에 전달 - 종료 코드를 커스텀 오류 유형으로
main()
에 다시 반환 - 비즈니스 로직을 다른 추상 수준에 둠
package main
이 지침은 main()
에서 실제 종료 흐름에 대한 장소가 있는 것만 요구합니다.