Programming Language/go

Go 언어 프로그래밍 - 에러처리

김크리 2021. 8. 2. 23:25

『Tucker의 Go 언어 프로그래밍』 스터디 요약 노트입니다.


 

에러는 언제 어디서나 발생한다.

오류(Error)

  • 응용 프로그램의 사용자에 의해 발생
  • 프로그래머가 적절한 예외처리를 하지 않은 경우 에러가 발생

버그(Bud)

  • 프로그래머의 실수
  • 디자인/ 기획단의 실수
  • 실수로 인한 오동작

에러 핸들링(Error Handling)

에러는 언제 어디서나 발생한다.

에러는 어디서든 발생할 수 있다. 그러므로 어떻게 에러에 대처하고 관리하는 것이 중요하다.

에러 핸들링은 크게 두가지이다. (사느냐 죽느냐)

  • 빠르게 프로그래밍을 죽이는 방법
  • 빠르게 에러를 처리하여 프로그램을 지속시키는 방법(에러 반환)

에러마다 개발 단계마다 프로그램 성격마다 처리 방법이 다르다.

에러 반환

호출자에게 에러를 반환하여 처리를 위임한다.

package main

import (
	"bufio"
	"fmt"
	"os"
)

func ReadFile(filename string) (string, error) {
	file, err := os.Open(filename)
	if err != nil {
		return "", err
	}
	//close file
	defer file.Close()

	rd := bufio.NewReader(file)

	//한줄씩 읽고 반환
	line, _ := rd.ReadString('\n')
	return line, nil
}
func WriteFile(filename string, line string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	//close file
	defer file.Close()

	//한줄을 쓰고 반환
	_, err = fmt.Fprintln(file, line)
	return err
}

const filename string = "data.txt"

func main() {
	line, err := ReadFile(filename)
	if err != nil {
		err = WriteFile(filename, "This is WriteFile.")
		if err != nil {
			fmt.Println("파일 생성에 실패하였습니다.", err)
			return
		}
		line, err = ReadFile(filename)
		if err != nil {
			fmt.Println("파일 읽기에 실패하였습니다.", err)
			return
		}
	}
	fmt.Println("파일 내용은 :", line)
}

사용자에러 반환

사용자가 에러를 만들어 반환할 수 있다.

fmt.Errorf(formatter string, ...interface{}) error 함수
OR
errors.New(text string) error 함수
package main

import (
	"fmt"
	"math"
)

func Sqrt(f float64) (float64, error) {
	if f < 0 {
		return 0, fmt.Errorf("제곱근은 양수여야 한다. f:%g", f)
        //return errors.New("제곱근은 양수여야 한다.")
        //errors.New 내부는 값을 반환할 수 없다. (string 만 가능)
	}
	return math.Sqrt(f), nil
}
func main() {
	sqrt, err := Sqrt(-2)
	if err != nil {
		fmt.Printf("Error : %v\n", err)
		return
	}
	fmt.Printf("Sqrt(-2) = %v\n", sqrt)
}
package main

import (
	"fmt"
)

type PasswordError struct {
	Len        int
	RequireLen int
}

func (err PasswordError) Error() string {
	return "암호 길이가 짧습니다."
}
func RegisterAccount(name, password string) error {
	if len(password) < 8 {
		//custom error 반환
		//아래 세가지는 다 동일하다.
		//return fmt.Errorf("~")
		//return errors.New("~")
		//단, 에러 객체를 직접 만들어 반환하는 것일 뿐이다.
		return PasswordError{len(password), 8}
	}
	return nil
}
func main() {
	err := RegisterAccount("myID", "myPw")
	if err != nil {
		//타입변환 성공여부 확인과 성공시 객체 반환
		if errInfo, ok := err.(PasswordError); ok {
			fmt.Printf("%v Len:%d RequireLen:%d\n", errInfo, errInfo.Len, errInfo.RequireLen)
		} else {
			fmt.Println("회원가입되었습니다.")
		}

	}
}

에러래핑(Error Wrapping)

fmt.Errorf()의 %w 포맷터로 에러 래핑 가능

error.ls()함수와 as()함수로 래핑을 꺼내 올 수 있다.

package main

import (
	"bufio"
	"errors"
	"fmt"
	"strconv"
	"strings"
)

//에러처리에 관한 예제 + string 처리에 관한 예제
func MultipleFromString(str string) (int, error) {
	//scanner는 일정한 한줄/한구문씩 string을 가져오기 좋은 함수이다.
	scanner := bufio.NewScanner(strings.NewReader(str))
	scanner.Split(bufio.ScanWords)

	pos := 0
	a, n, err := readNextInt(scanner)
	if err != nil {
		return 0, fmt.Errorf("Failed to readNextInt(), pos: %d err:%w", pos, err)
	}
	pos += n + 1
	b, n, err := readNextInt(scanner)
	if err != nil {
		return 0, fmt.Errorf("Failed to readNextInt(), pos: %d err:%w", pos, err)
	}
	return a * b, nil
}

//다음 단어를 읽서 숫자로 변환하여 반환한다.
//반환된 숫자, 읽은 글자 수, 에러를 반환합니다.
func readNextInt(scanner *bufio.Scanner) (int, int, error) {
	if scanner.Scan() {
		return 0, 0, fmt.Errorf("Failed to scan")
	}
	//읽은 값을 문자열로 받아온다.
	word := scanner.Text()
	//문자열을 숫자(integer)로 바꾼다. -> 숫자모양의 문자가 아닐 경우, 에러 반환
	numer, err := strconv.Atoi(word)
	if err != nil {
		return 0, 0, fmt.Errorf("Failed to convert word to int, word:%s, err:%w", word, err)
	}
	return numer, len(word), nil
}
func readEq(eq string) {
	rst, err := MultipleFromString(eq)
	if err == nil {
		fmt.Println(rst)
	} else {
		fmt.Println(err)
		var numError *strconv.NumError
		//errors.As 를 이용하여 내부에서 에러 래핑되어있는 것을 변환해서 넣어준다.
		if errors.As(err, &numError) {
			fmt.Println("NumberError", numError)
		}
	}
}
func main() {
	readEq("123 3")
	readEq("123 abc")
}

패닉

처리하기 힘든 에러를 만났을 때 프로그램을 조기 종료하는 방법

빠르게 종료시켜서 오류를 해결하기 위해서 사용

일반에 공개 된 뒤에는 최대한 안죽는것이 중요하다.(패닉 활용에 적절하지 않다.)

일반에 공개 된 뒤, 패닉(panic)을 복구하는 것이 필요하다.(모든 패닉 코드 수정에 어려움)

package main

import (
	"errors"
	"fmt"
)

func divide(a, b int) {
	if b == 0 {
		//패닉 셋팅
		panic("b는 0일 수 없습니다.")
	}
	fmt.Printf("%d / %d = %d\n", a, b, a/b)
}

func divide2(a, b int) error {
	if b == 0 {
		return errors.New("b는 0일 수 없습니다.")
	}
	fmt.Printf("%d / %d = %d\n", a, b, a/b)
	return nil
}
func main() {
	divide2(9, 3)
	divide2(9, 0)
	divide(9, 3)
	divide(9, 0)
	//실행시, 어디서 패닉이 일어났는지 알려준다.
	//패닉은 패닉 시점에서 프로그램을 종료한다.
}

패닉 복구

복구는 recover()라는 함수를 사용한다. 패닉 객체를 반환한다.

패닉의 발생 역순으로 진행하며 최종적으로 복구가 되지 않으면 프로그램이 종료된다.(복구 실패)

Defer와 함께 사용된다.

package main

import (
	"fmt"
)

func f() {
	fmt.Println("f() 함수 시작")
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("panic 복구 - ", r)
		}
	}()
	g()
	fmt.Println("f() 함수 끝")
}
func g() {
	fmt.Printf("9/3 = %d\n", h(9, 3))
	fmt.Printf("9/0 = %d \n", h(9, 0))
}
func h(a, b int) int {
	if b == 0 {
		panic("제수는 0 일 수 없습니다.")
	}
	return a / b
}
func main() {
	f()
	fmt.Println("프로그램이 계속 실행됨")
}

 

Go는 SEH(Structured Error Handling, 구조화된 에러처리)을 지원하지 않는다.

SEH의 단점은 대표적으로 두가지가 있다.

  • 성능문제
  • 에러를 먹어버리는 문제(에러 처리를 등한시 한다.)
//SEH 예시
try {
...
}
catch(...){}
finally{}

에러처리는 매우 중요하다.

에러처리는 중요한 코드의 일부분으로 여기고 에러를 반환하는 함수에서 반환 되는 에러를 제대로 처리해야 한다.(_, 빈칸 지시자로 무시하면 안된다.)

에러는 드러내야 하고 조기에 발견하여 더 큰문제를 미연에 방지해야 한다.

참고

Tucker의 Go언어 프로그래밍 - Go가 온당

https://www.youtube.com/c/TuckerProgramming/videos