Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.
에러 핸들링(Error handling)
- 프로그램의 에러를 처리하는 방법을 의미
- 에러는 프로그래머의 실수로 발생하지만, 때로는 외부적 요인(메모리/디스크 부족, 네트워크 단절 등)에 의해 발생되기도 함.
- 에러가 발생하면 경우에 따라 프로그램을 종료하거나 적절한 방식으로 처리하여 프로그램을 계속 실행시킬 수 있음.
에러 반환
- 에러를 처리하는 가장 기본 방식은 에러를 반환하고 알맞게 처리하는 방식
ex) ReadFile() 함수로 파일을 읽을 때 해당하는 파일이 없어 에러가 발생
package main
import (
"bufio"
"fmt"
"os"
)
func ReadFile(filename string) (string, error) {
file, err := os.Open(filename) // 파일 열기
if err != nil {
return "", err // 에러 나면 에러 반환
}
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
}
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) // 파일 내용 출력
}
// 결과
파일내용: This is WriteFile // open할 파일이 없었음
✓ ReadFile()은 filename에 해당하는 파일을 읽어서 반환하는 함수로, 파일 읽기에 실패하면 에러를 두 번째 반환값으로 반환함.
- os.Open() 함수로 파일을 열고, err이 nil이면 성공, 아니면 에러가 발생한 것(파일 열기에 실패), file.Close()함수는 파일 핸들을 닫음.
= 파일 열기에 실패하면 호출자에게 에러가 발생함을 알려줌.
- bufio.NewReader() 함수로 bufio.Reader 객체를 만듦(해당 객체는 구분자까지 문자열을 읽어오는 ReadString() 메서드가 있음)
= 해당 메서드를 통해 '\n'이 나올 때까지 파일에서 문자열을 읽음(개행으로 한 줄을 읽음)
- bufio.ReadString()은 첫 번째 반환값으로 읽은 문자열을, 두 번째 반환값으로 error를 반환
= error는 문자열이 delim 문자로 끝나지 않을 경우에만 에러를 반환, _을 사용하여 에러 무시처리
- WriteFile() 함수는 filename에 해당하는 파일을 생성하고, 생성한 파일에 line 문자열을 씀.
- os.Create() 함수는 파일을 생성하고, 첫 번째 반환 값으로 파일 핸들 두 번째 반환 값으로 에러를 반환.
= 파일 생성에 실패하면, 에러를 반환하여 호출자에서 처리를 요청함.
- fmt.Fprintln() 함수를 사용해 파일 핸들에 문자열과 줄바꿈 문자 "\n"을 씀.
- 첫 번째 인수로 Write() 메서드를 가진 io.Writer 인터페이스를 인수로 받음.
= Write() 메서드를 가진 모든 객체는 Fprintln() 함수 인수로 쓸 수 있음.
파일 핸들인 *File 타입도 Write() 메서드를 가지고 있기 때문에 인수로 사용할 수 없음.
- 두 번째 인수부터는 쓸 내용이 들어가고, fmt.Fprintln() 함수는 첫 번째 반환값으로 쓴 길이를 반환하고 두 번째로 에러 발생 시 에러를 반환.
= 쓴 글이는 필요 없어 _로 무시하고, 두 번째 에러만 반환
- main() 함수에서 ReadFile()로 파일 읽기를 시도하고 에러가 발생하면 WriteFile() 함수를 호출하여 파일을 생성함.
- 파일 생성 시에도 에러가 발생하면 에러 메시지를 출력하고 프로그램을 종료
- 파일 생성에 성공하면, ReadFile() 함수로 파일 읽기를 시도하고, 읽은 파일 내용을 출력하며 프로그램을 종료함.
사용자 에러 반환
- OS 패키지의 Open() 함수와 Create() 함수에서 발생한 에러 처리 방법 예제
fmt.Errorf(formatter string, ...interface{}) error
→ 에러를 만들어주는 함수로, 반환 타입이 error임.
- formatter string과 인자를 사용하여 해당 포멧으로 에러 메시지를 만듦.
errors.New(text string) error
→ errors 패키지의 New() 함수를 이용해서 error를 생성할 수 있음.
- 인수로 문자열을 입력하면 인수와 같은 메시지를 갖는 error를 생성하여 반환.
package main
import (
"fmt"
"math"
)
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, fmt.Errorf(
"제곱근은 양수여야 합니다. f:%g", f) // f가 음수이면 에러 반환
}
return math.Sqrt(f), nil
}
func main() {
sqrt, err := Sqrt(-1)
if err != nil {
fmt.Printf("Error: %v\n", err) // 에러 출력
return
}
fmt.Printf("Sqrt(-2) = %v\n", sqrt)
}
// 결과
Error: 제곱근은 양수여야 합니다. f:-1
- Sqrt() 함수는 인수의 제곱근을 반환하고, 인수 f가 음수이면 에러를 반환
에러 타입
- error는 interface로 문자열을 반환하는 Error() 메서드로 구성되어 있음.
- 즉, 어떤 타입이든 문자열을 반환하든 Error() 메서드를 포함하고 있다면 에러로 사용할 수 있음.
type error interface {
Error() string
}
package main
import "fmt"
type PasswordError struct { // 에러 구조체 선언
Len int
RequireLen int
}
func (err PasswordError) Error() string { // Error() 메서드
return "암호 길이가 짧습니다."
}
func RegisterAccount(name, password string) error {
if len(password) < 8 {
return PasswordError{len(password), 8} // error 반환
}
return nil
}
func main() {
err := RegisterAccount("myID", "myPw") // ID, PW 입력
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("회원 가입됐습니다.")
}
}
// 결과
암호 길이가 짧습니다. Len:4 RequireLen:8
- 암호 길이가 짧을 때 에러 정보를 담을 PasswordError 구조체를 선언하고, 이는 실제 입력한 길이와 필요한 길이를 필드로 가짐.
- PasswordError 구조체 메서드로 Error() 함수를 선언했기 때문에 PasswordError 구조체는 error 인터페이스로 사용될 수 있음.
- RegisterAccount() 함수는 암호 길이가 짧을 때 PasswordError 구조체 정보를 반환
= RegisterAccount() 함수의 반환 타입은 error 타입이지만, PasswordError는 error 인터페이스로 쓸 수 있음. (구조체 반환 가능)
- RegisterAccount() 함수로 ID와 PW를 입력하고, 반환값이 nil이 아니면 에러가 발생됨.
- 에러를 PasswordError 타입으로 타입 변환하여 에러 메시지뿐만 아니라 Len과 RequireLen 필드에 접근 가능.
에러 랩핑
- 때로는 에러를 감싸서 새로운 에러를 만들어야 할 수도 있음.
package main
import (
"bufio"
"errors"
"fmt"
"strconv"
"strings"
)
func MultipleFromString(str string) (int, error) {
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()
number, 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 number, 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
if errors.As(err, &numError) { // 감싸진 에러가 NumError인지 확인
fmt.Println("NumberError:", numError)
}
}
}
func main() {
readEq("123 3")
readEq("123 abc")
}
// 결과
369
Failed to readNextInt(), pos:4 err:Failed to convert word to int, word:abc err:strconv.Atoi: parsing "abc": invalid syntax
NumberError: strconv.Atoi: parsing "abc": invalid syntax
- MultipleFromString() 함수는 문자열에서 두 단어를 읽어 각 숫자로 변환한 뒤 곱한 결과를 반환하는데, 만약 과정에서 에러가 발생하면 에러를 반환.
- 한 단어씩 읽는 bufio 패키지의 Scanner를 만들고, NewScanner() 함수는 io.Reader 인터페이스를 인수로 받기 때문에
string 타입을 io.Reader로 만들어 주려고 string.NewReader()함수를 사용.
= ★ 문자열을 한 줄씩 또는 한 단어씩 끊어 읽고자 할 때 주로 사용하는 구문 ★
- Scanner 객체의 Split() 메서드를 호출해 어떻게 끊어 읽을지 알려줌.
- bufio.ScanWords를 사용하면 한 단어씩 끊어 읽게 되고 bufio.ScanLines를 사용하면 한 줄씩 끊어 읽게 됨.
- Scan() 메서드를 통해 첫 번째 단어를 읽어오는데, 만약 "123"을 읽으면 문자열이기 때문에 int 타입인 숫자로 변경해야 함.
- ★ strconv 패키지의 Atoi() 함수는 문자열을 int 타입으로 변경. (Itoa() 함수는 반대로 숫자를 문자열로 바꿈) ★
- Atoi() 함수는 숫자로 변경 시 숫자가 아닌 문자가 섞여있는 경우 NumberError 타입의 에러를 반환함.
= 에러가 발생한 경우 fmt.Errorf() 함수의 %w 서식문자를 통해 에러를 감싸줌.
fmt.Errorf("Error: %w", err)
→ 위와 같이 %w를 사용하면, err를 감싸서 새로운 에러를 반환하게 됨.
- 다른 정보와 strconv.Atoi() 함수에서 발생한 에러까지 에러 하나로 반환할 수 있게 됨.
- readNewInt() 함수에서 발생한 에러를 다시 감싸 pos 정보를 에러에 추가 함.
= 출력 결과에 네 번째 글자를 읽을 때 에러가 발생했음을 알 수 있음.
- 감싸진 에러를 다시 꺼낼 때는 errors 패키지의 As() 함수를 이용함.
- errors.As()는 err 안에 감싸진 에러 중 두 번째 인수의 타입인 *strconv.NumError로 변환할 수 있는 에러가 있으면 변환하여 값을 넣고,
true를 반환함
- 코드에서 감싼 에러가 strconv.Atoi() 함수에서 발생한 에러이기 때문에 이 에러는 *strconv.NumError 타입이라, errors.As() 함수는 true를 반환하고,
numError에 *strconv.NumError 타입 값을 넣어 줌.
= 감싸진 에러를 검사하고 각 에러 타입별로 다른 처리를 할 수 있음.
패닉(Panic)
- 프로그램을 정상 진행 시키기 어려운 상황을 만났을 때 프로그램 흐름을 중지시키는 기능
- Go언어는 내장 함수 panic()으로 패닉 기능을 제공
- Panic() 내장 함수를 호출하고 인수로 에러 메시지를 입력하면, 프로그램을 즉시 종료하고 에러 메시지를 출력하고,
함수 호출 순서를 나타내는 콜 스택(Call Stack)을 표시 함. (이 정보를 사용해 에러가 발생한 경로를 파악할 수 있음)
package main
import "fmt"
func divide(a, b int) {
if b == 0 {
panic("b는 0일 수 없습니다.") // Panic 발생
}
fmt.Printf("%d / %d = %d\n", a, b, a/b)
}
func main() {
divide(9, 3)
divide(9, 0) // Painc 발생
}
// 결과
9 / 3 = 3
panic: b는 0일 수 없습니다.
goroutine 1 [running]:
main.divide(0x9?, 0x3?)
/tmp/sandbox1432610449/prog.go:7 +0xee
main.main()
/tmp/sandbox1432610449/prog.go:14 +0x29
- divide() 함수의 제수를 나타내는 b가 0이면, panic() 함수를 호출해 프로그램을 강제 종료함.
- panic() 함수의 인수로 에러가 발생한 원인을 적을 수 있음.
- 콜 스택이란 panic이 발생한 마지막 함수 위치부터 역순으로 호출 순서를 표시함.
패닉 생성
- 패닉은 내장 함수 panic()을 사용해서 발생시킬 수 있음.
- panic() 내장 함수의 인수로 interface{} 타입 = 모든 타입을 사용할 수 있음.
- 일반적으로 string 타입 메시지나 fmt.Errorf() 함수를 이용해서 만들어진 에러 타입을 주로 사용함.
// Panic 함수 선언
func panic(interface{})
// Panic 함수 예시 구문
panic(42)
panic("unreachable")
panic(fmt.Errorf("This is error num:%d", num)
panic(SomeType{SomeData})
패닉 전파 그리고 복구
- 프로그램을 개발할 때는 문제를 빠르게 파악해서 해결 하는 것이 중요!
* 패닉(Panic)의 전파
- 패닉의 전파는 콜스택 역순으로 전파가 됨.
ex) 함수 호출 과정에서 main() → f() → g() → h() 중 h() 함수에서 패닉이 발생하면 호출 순서를 거꾸로 올라가며 함수로 전달
main() 함수에서까지 복구되지 않으면 프로그램이 그제서야 강제 종료 됨.
* 패닉(Panic)의 복구(recover)
- 어느 단계에서든 패닉은 복구된 시점부터 프로그램이 계속됨.
- recover() 함수를 호출해 패닉 복구를 할 수 있음.
- recover() 함수가 호출되는 시점에 패닉이 전파중이면 panic 객체를 반환하고, 그렇지 않으면 nil을 반환.
- recover 함수는 defer 함수와 함께 사용 함!!!
func recover() interface{}
package main
import "fmt"
func f() {
fmt.Println("f() 함수 시작")
defer func() { // 패닉 복구
if r := recover(); r != nil {
fmt.Println("panic 복구 -", r)
}
}()
g() // g() → h() 순서로 호출
fmt.Println("f() 함수 끝")
}
func g() {
fmt.Printf("9 / 3 = %d\n", h(9, 3))
fmt.Printf("9 / 0 = %d\n", h(9, 0)) // h() 함수 호출 - 패닉
}
func h(a, b int) int {
if b == 0 {
panic("제수는 0일 수 없습니다.") // 패닉 발생 !!
}
return a / b
}
func main() {
f()
fmt.Println("프로그램이 계속 실행됨") // 프로그램 실행 지속됨
}
// 결과
f() 함수 시작
9 / 3 = 3
panic 복구 - 제수는 0일 수 없습니다.
프로그램이 계속 실행됨
- main() 함수에서 f() 함수를 호출하고 다시 g() →h() 순서로 함수가 호출 됨.
- 두 번째 h() 함수 호출 시 두 번째 인수값이 0이라서 패닉이 발생
- h() 함수에 패닉이 발생하여 호출 순서를 거슬러 전파 됨. = g() → f() → main()으로 전파
- 패닉이 f() 까지 전파 됐으나 defer를 사용해 함수 종료 전 함수 리터럴이 실행됨.
- 함수 리터널 내부에서 recover()를 사용해 패닉 복구를 시도하고, 전파중인 패닉이 있으므로 복구가 되고 panic 메시지 출력함.
- 패닉이 f() 함수에서 복구됐기 때문에 프로그램이 강제 종료되지 않고 계속 실행됨.
= 그래서 "프로그램이 계속 실행됨" 메시지가 출력됨.
※ recover()는 제한적으로 사용하는 것이 좋음.
패닉이 발생되면 그 즉시 호출 순서를 역순으로 전파하기 때문에 복구가 되더라도 프로그램이 불안정할 수 있음.
문제가 발생되면 개발자가 빨리 인지를 하고 수정하는 것이 서비스에 더 낫고, recover()되면 문제가 있다는 것을 인지할 수 없음. ※
Go는 SEH를 지원하지 않음
- SEH(Structured Error Handling) 구조화된 에러 처리를 지원하지 않음.
- 성능 문제와 ※에러를 먹어버리는 문제(오히려 에러 처리를 등한시 함)※ 로 지원하지 않음.
'Language > Golang' 카테고리의 다른 글
Golang (Go언어) 채널과 컨텍스트(Channel & Context) (0) | 2024.12.11 |
---|---|
Golang (Go언어) Go루틴 (0) | 2024.12.07 |
Golang (Go언어) 자료 구조(Data Structure) 2/2 (0) | 2024.12.03 |
Golang (Go언어) 자료 구조(Data Structure) 1/2 (1) | 2024.12.02 |
Golang (Go언어) 함수고급편 (1) | 2024.11.29 |