에러 처리

에러 처리

에러 처리에 대해 알아보자.


에러 처리의 기본


실습 문제의 목표는 각 챕터에서 배운 용법을 어느 문법으로 직접 활용해보는 것이었기 때문에 예외 처리하는 부분에서 허점이 많이 있었습니다. 에러 처리를 하는 이유는 컴파일러가 알아차리지 못하는 프로그램상의 오류를 예방하기 위해서입니다. 따라서 반환값이 있는 함수는 에러 처리(논리상 예외가 있을만한 부분을 에러 처리)를 통해 결괏값과 에러 값을 함께 반환해야합니다.
Go언어 패키지에서 제공하는 표준 함수는 어떻게 에러 처리를 하는지 알아보겠습니다. 우리가 항상 사용했던 코드가 에러 처리가 되어있는 코드입니다.

package main

import "fmt"

func main() {
	var input string
	_, err := fmt.Scanln(&input);
	
	if err != nil {
		panic(err)
	}
	
	fmt.Println(input);
}

우리는 지금까지 fmt.Scanln() 함수를 사용자로부터 값을 입력받는 용도로만 사용해왔습니다. 그래서 이 함수가 반환값이 없는 것으로 알고 있었습니다. 하지만 Scanln() 함수를 포함한 “fmt” 패키지의 표준 입/출력 함수는 첫 번째 입력 개수, 두 번째 반환값은 에러 값과 같이 모두 반환 값을 가집니다.

아래 예시는 다양한 표준 함수들이 반환 값이 존재한다는 것정도로 확인하기 바랍니다.
"fmt" 패키지에서 제공하는 함수입니다.(전부는 아님)

func Scan(a ...interface{}) (n int, err error)
func Scanln(a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
func Print(a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
아래는 "os" 패키지에서 제공하는 파일 처리 함수입니다.(전부는 아님)
func Create(name string) (file *File, err error)
func Open(name string) (file *File, err error)
func (f *File) Close() error
모든 함수의 상세 정보를 알고싶다면 [Go언어 공식 사이트](https://go.dev) 에서 패키지를 확인하세요.

사용자에게 두 실수를 입력받고 나눗셈을 하는 프로그램을 만든다고 생각해봅시다. 이때, 나누는 수를 0으로 하면 무한대가 되기 때문에 에러 처리를 하도록 하고싶습니다. 그럼 두 가지를 설정해야합니다.

1. 어떻게 에러 값을 설정할 것인가?
2. 어떻게 에러 상황을 출력하고 처리할 것인가?

에러 값 설정


에러 값을 설정하기에 앞서 error 타입이라는 것이 무엇인지 알아야합니다. error는 우리가 일반적으로 생각하는 자료형이 아닌 인터페이스형입니다. error 인터페이스는 Error() 라는 string형을 반환값으로 갖는 메소드를 한개만 가지고 있습니다.

type error interface {
	Error() string
}

func (e *errorString) Error() string {
	return e.s
}

type errorString struct {
	s string
}

그렇다면 어떻게 구조체 errorString 포인터를 초기화할 수 있는지 알아보겠습니다. "errors" 패키지의 New() 함수를 이용하면 됩니다. errors.New("에러값") 형태로 입력해 함수를 호출하면 아래 형식과 같이 “에러값”이 errorString 구조체 형으로 변환되고 포인터를 반환합니다.

func New(text string) error {
	return &errorString(text)
}
package main

import (
	"fmt"
	"errors"
)

func divide(a float32, b float32) (result float32, err error) {
	if b == 0 {
		return 0, errors.New("0으로 나누지마") 
	}
	result = a / b
	return 
}

func main() {
	var num1, num2 float32
	fmt.Scanln(&num1, &num2)
	
	result, err := divide(num1, num2)
	
	if err != nil {
		panic(err)
	}
	
	fmt.Println(result)
}

에러 출력 및 처리


그동안 Panic을 이용해서 panic(err) 형식으로 에러 값을 출력하고 panic을 발생시켜 프로그램을 종료하는 방식으로 에러를 처리했습니다. 물론 이 방법도 틀린 방법은 아닙니다. 그렇지만 주로 에러를 처리하는 다른 방법이 있기 때문에 다른 형식을 확인해보겠습니다.
아래는 "log" 패키지에서 제공하는 에러 출력 함수입니다. 각 함수는 에러 값을 출력하고 처리하는 방식이 다릅니다. 당연히 이 함수들을 사용하기 위해서 “log”를 import 해야합니다.

func Fatal(v ...interface{}) : 에러 로그 출력  프로그램 종료
func Panic(v ...interface{}) : 시간, 에러 메시지 출력  패닉 발생, defer 구문이 없을  런타임 패닉을 발생시키고 콜스택 출력
func Print(v ...interface{}) : 시간, 에러 메시지 출력 하지만 프로그램 종료하지 않음
package main

import (
    "fmt"
    "log"
)

func divide(a float32, b float32) (result float32, err error) {
    if b == 0 {
        return 0, fmt.Errorf("%.2f으로 나누지마", b) 
    }
    result = a / b
    return 
}

func main() {
    var num1, num2 float32
    fmt.Scanln(&num1, &num2)

    result, err := divide(num1, num2)

    if err != nil {
        log.Print(err)
    }

    fmt.Println(result)
}

fmt.Errorf("%.2f으로 나누지마", b) 형식을 이용해 에러 값의 포멧을 지정해 저장하고, log.Print(err) 형식을 이용해 시간, 에러 메시지를 출력 하지만 프로그램은 종료하지 않습니다. 그래서 fmt.Println(result)이 실행됩니다.

package main

import (
    "fmt"
    "log"
)

func errorChek(n int) (string, error) {
    if n == 1 {       
        s := "Goorm"
        return s, nil // 정상 동작이므로 에러 값은 nil
    }

    return "", fmt.Errorf("%d는 1이 아닙니다.", n) // 1이 아닐 때는 에러 리턴
}

func main() {
    s, err := errorChek(1) // 매개변수에 1을 넣었으므로 정상 동작
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(s) // Hello 1

    s, err = errorChek(2)     // 매개변수에 2를 넣었으므로 에러 발생
    if err != nil {
        log.Print(err)
    }
    fmt.Println(s)
    
    defer func() {      
        s, err = errorChek(4)     // 매개변수에 4를 넣었으므로 에러 발생
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(s)
    }()
    
    
    s, err = errorChek(3)     // 매개변수에 3을 넣었으므로 에러 발생
    if err != nil {
        log.Panic(err)   // defer 함수로 이동
    }
    fmt.Println(s)
    
    // 에러가 발생하여 프로그램이 종료되었으므로 이 아래부터는 실행되지 않음
    fmt.Println(s)

    fmt.Println("Hello, world!")
}

log.Print(err)에러 메시지만 출력합니다. 따라서 중요하지 않은 문제에만 적용하는 것이 좋습니다. 그다음 log.Panic(err)이 실행되고 panic이 발생하기 전에 defer 구문을 실행합니다. 마지막으로 defer 구문의 익명 함수 안에 있는 log.Fatal(err)이 실행되고 프로그램이 완전히 종료됩니다.

그리고 log.Panic(err)과 log.Fatal(err)차이로는 Panic은 런타임 에러를 발생시키고 프로그램을 종료하고 Fatal은 프로그램을 정상적으로 완전히 종료합니다. 그리고 log.Panic()과 Panic() 함수는 같은 역할을 합니다.

package main

import (
    "fmt"
    "log"
)

func errorChek(score int) (int, error) {
    if score >= 0 {
        return score, nil
    }
    return 0, fmt.Errorf("시험 점수를 양의 정수로 입력하세요.")
}

func main() {
    var score int
    fmt.Scanln(&score)
    
    s, err := errorChek(score)
    
    if err != nil {
        log.Panic(err)
    }
    fmt.Printf("시험 점수는 %d점입니다.",s)
}

© 2022. All rights reserved. 신동민의 블로그