Golang Error 처리

노재원·2020년 8월 30일
1

Go

목록 보기
1/1
post-thumbnail

시작하기 앞서...

프로그래밍 공부할 당시에는 에러처리를 귀찮다는 이유로 거의 안했다가 최근에 프로젝트를 진행하면서 에러처리의 중요성을 깨달았다. 시간이 지날 수록 내가 짠 코드의 빈틈이 드러나고 그에 대한 에러처리가 안돼있어서 고생을 많이 한 기억이 있기 때문이다. golang을 이제 막 공부하기 시작했고, 이제는 에러 처리의 중요성을 알고 있기 때문에 에러 관련해서 정리를 해 볼 것이다.
'Go 마스터하기' 책에 있는 예제 코드를 기반으로 글을 작성했다.

error 타입

golang에서는 데이터 타입으로 error 타입을 제공한다. logic이나 잘못된 함수 사용으로 인해 발생하는 error들이 있고, 사용자가 정의한 error가 있다. 우선 사용자 정의 error부터 살펴보자.

사용자 정의 error

사용자가 error를 정의하기 위해서 error 변수를 생성해야되고, error 변수를 생성하려면 'errors'라는 패키지를 사용해야 한다.

import(
   "errors"
)
err := errors.New("Error is occurred!")

사용법이 상당히 간단하다. 이번엔 간단한 예제를 통해서 error를 생성하고 출력해보자.

func returnError(a, b int) error{
	if a == b {
    	err := errors.New("Error in returnError() functions")
        return err
    } else {
    	return nil
    }
}

func main(){
	err := returnError(1, 2)
    if err == nil{
    	fmt.Println("End normally")
    } else {
    	fmt.Println(err)
    } //"End normally" 출력
    
    err = returnError(10, 10)
    if err == nil{
    	fmt.Println("End normally")
    } else {
    	fmt.Println(err)
    } //"Error in returnError() function" 출력
}

실행해보면 예상했다 시피 숫자가 다를 땐 End normally가, 같을 땐 error 변수를 생성하며 입력한 error 문구가 출력된다.
추가로 아래와 같이 Error() 메소드를 사용하면 error 문구가 string 형으로 반환되며, 다른 string과 비교할 수도 있다.

fmt.Println(err.Error()) //"Error in returnError() function" 출력

잘못된 함수 사용 error

n, err := strconv.ParseFloat("a", 64)
if err != nil{
	fmt.Println(err)
} else {
	fmt.Println(n)
} //"strconv.ParseFloat: parsing "a": invalid syntax" 출력

strconv의 ParseFloat함수는 (parsing 결과 값, error)를 반환한다. 따라서 개발자는 쉽게 error 처리를 할 수 있다.

try ~ catch ?

지금까지는 error 처리라기 보다는 error 판별? 정도이다. 진정한 error 처리라고 하면 error가 발생했을 때 그에 대한 후속조치를 하면서 프로그램이 죽지않도록 하는 것이다. 보통 그런 목적으로 try ~ catch ~ finally문을 사용해왔다. 하지만 golang에는 try~catch문이 존재하지 않는다. 그렇다면 어떻게 error 처리를 할 수 있을지 알아보자.

defer 키워드

본격적으로 시작하기 앞서 defer 키워드에 대한 개념을 잡고 가야 한다.

defer 키워드가 붙은 func A는 func A를 담고 있는 함수 func R이 리턴될 때까지 func A의 실행을 미룬다. JAVA나 C#의 finally 구문과 유사하다

말로만 들으면 이해가 안가니 예제를 하나 보자

func d1(){
    for i:=3; i > 0; i--{
    	defer fmt.Print(i, " ")
    }
} //1 2 3

func d2(){
    for i:=3; i>0; i--{
    	defer func(){
        	fmt.Print(i, " ")
        }()
    }
}// 0 0 0

func d3(){
    for i:=3; i>0; i--{
    	defer func(n int){
        	fmt.Print(n, " ")
        }(i)
    }// 1 2 3
}

defer의 특징이 있는데, defer가 여러 번 호출되면 stack처럼 가장 늦게 호출된 것이 가장 먼저 실행된다는 점이다. 이제 함수를 하나씩 보면서 왜 저렇게 출력됐는지 살펴보자.

func d1(){
    for i:=3; i > 0; i--{
    	defer fmt.Print(i, " ")
    }
} //1 2 3

fmt.Print함수가 3번 호출되면서 stack에 쌓인다고 생각해보자. 그러면 stack에는 fmt.Print(3, " ") -> fmt.Print(2, " ") -> fmt.Print(1, " ") 순으로 들어간다. d1함수가 끝나면 반대로 fmt.Print(1, " ") -> fmt.Print(2, " ") -> fmt.Print(3, " ") 순서로 함수들이 실행 될 것이고 "1 2 3"이 출력된다.

func d2(){
    for i:=3; i>0; i--{
    	defer func(){
        	fmt.Print(i, " ")
        }()
    }
}// 0 0 0

d1와 똑같이 stack의 상태를 보자. fmt.Print(i, " ") -> fmt.Print(i, " ") -> fmt.Print(i, " ") 순으로 쌓일 것이다. 그리고 d2가 끝나면서 호출될 때 i는 0이다. 따라서 출력 결과는 "0 0 0"이 된다.

func d3(){
    for i:=3; i>0; i--{
    	defer func(n int){
        	fmt.Print(n, " ")
        }(i)
    }// 1 2 3
}

d2와 달리 d3는 그때그때 i값을 매개변수로 받았다. 따라서 stack에는 fmt.Print(3, " ") -> fmt.Print(2, " ") -> fmt.Print(1, " ")순으로 쌓이고 결과는 d1과 똑같이 나오게 된다.

panic과 recover 함수

둘 다 golang에서 기본으로 지원하는 함수로써, panic은 프로그램 실행을 종료하고 패닉 상태에 빠지고, recover은 패닉 상태에 있는 Go 루틴으로부터 다시 제어권을 가져오게 한다. 예제를 살펴보자

func a(){
    fmt.Println("Inside a()")
    defer func(){
    	if c:= recover(); c != nil{
        	fmt.Println("Recover inside a()")
        }
    }()
    fmt.Println("About to call b()")
    b()
    fmt.Println("b() exited")
    fmt.Println("Exitting a()")
}

func b(){
    fmt.Println("Inside b()")
    panic("Panic in b()!")
    fmt.Println("Exitting b()")
}

func main(){
    a()
    fmt.Println("main() ended")
}
/*출력 결과
Inside a()
About to call b()
Inside b()
Recover inside a()
main() ended
*/

a함수에서 b함수를 실행시켰는데 b함수에서 에러가 나면서 panic상태에 빠지고 실행이 종료됐다. a함수가 종료됐으므로 defer func함수가 실행되는데 recover함수를 통해 다시 제어권을 받아와 "Recover inside a()"를 출력하고 빠져나와 "main ended"문구를 출력하며 끝났다. 출력 문구를 보면 a함수의 마지막 2줄이 출력되지 않은 것으로 보아 a함수가 정상적으로 종료되지 않았다는 것을 확인할 수 있다.
만약 b함수에서 panic이 발생하지 않았다면 "Exitting b()", "b() exited", "Exitting a()" 가 출력되고 recover 함수의 반환값이 nil이기 때문에 "Recover inside a()"는 출력되지 않을 것이다.

golang error처리를 전반적으로 살펴봤는데, 사용하는데 있어서 상당히 간편하고 기존의 try catch문 보다 간편하다는 생각이 들었다. 역시 golang은 하면 할수록 매력이 있다.
전체 예제 코드는 여기에서 확인할 수 있다.

0개의 댓글