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{}
에러처리는 매우 중요하다.
에러처리는 중요한 코드의 일부분으로 여기고 에러를 반환하는 함수에서 반환 되는 에러를 제대로 처리해야 한다.(_, 빈칸 지시자로 무시하면 안된다.)
에러는 드러내야 하고 조기에 발견하여 더 큰문제를 미연에 방지해야 한다.
참고
https://www.youtube.com/c/TuckerProgramming/videos