성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>Golang - fmt.Errorf, errors.Is, errors.As 설명</h1> <p> 예를 들어, 간단하게 (존재하지 않는) 파일을 열어 에러가 발생했을 때를 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > package main import ( "fmt" "os" ) func main() { <span style='color: blue; font-weight: bold'>defer func() { if err := recover(); err != nil { fmt.Printf("main: %v", err) } }()</span> doWork() } func doWork() { <span style='color: blue; font-weight: bold'>_, err := os.Open("test.dat")</span> if err != nil { <span style='color: blue; font-weight: bold'>panic(err)</span> } } </pre> <br /> 우리가 여기서 하고 싶은 것은, panic 시 "사용자 오류 메시지"를 덧붙이고 싶은 것입니다. 이에 대해서는 다양한 방법이 있을 텐데요, 우선 struct로 감싸는 방식을 쓸 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > package main import ( "fmt" "os" "reflect" ) func main() { defer func() { if err := recover(); err != nil { if e, ok := <span style='color: blue; font-weight: bold'>err.(struct { msg string; err error })</span>; ok { fmt.Printf("struct error: %s, %v", e.msg, e.err) } } /* reflection을 써도 되고! if err := recover(); err != nil { <span style='color: blue; font-weight: bold'>r := reflect.ValueOf(err) innerError := reflect.Indirect(r).FieldByName("err") innerMsg := reflect.Indirect(r).FieldByName("msg")</span> fmt.Printf("main: %v\n", innerError) fmt.Printf("main: %v\n", innerMsg) } */ }() doWork() } func doWork() { file, err := os.Open("test.dat") if err != nil { panic(<span style='color: blue; font-weight: bold'>struct { msg string; err error }{"doWork failed", err}</span>) } defer file.Close() } </pre> <br /> 위의 코드에서 구조체만 명시적으로 정의하는 것으로 다음과 같이 바꿀 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>type WrapError struct { Message string; Err error }</span> /* 기왕 만든 구조체에 다음의 메서드만 추가하면 error 인터페이스를 따르게 됩니다. func (e *WrapError) Error() string { return "WrapError.Error(): " + e.Message + ": " + e.Err.Error() } */ func main() { defer func() { err := recover() if err == nil { return } <span style='color: blue; font-weight: bold'>if e, ok := err.(WrapError); ok {</span>} fmt.Printf("wrapError message == %v\n", e) fmt.Printf("main: %v\n", e.Err) fmt.Printf("main: %v\n", e.Message) } }() doWork() } func doWork() { file, err := os.Open("test.dat") if err != nil { <span style='color: blue; font-weight: bold'>panic(WrapError{"doWork failed", err})</span> } defer file.Close() } /* 출력 결과 wrapError message == {0xc000072480 doWork failed} main: open test.dat: The system cannot find the file specified. main: doWork failed */ </pre> <br /> 그런데, 사실 위와 같은 경우에 진짜 오류는 Err 필드에 담겨 있으므로 그것의 종류에 따라 처리하고 싶을 때가 있습니다. 당연히, 다음과 같은 식으로 직접 recover가 반환한 err에 대해 타입 조회를 할 수 없습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // ok == False if e, ok := <span style='color: blue; font-weight: bold'>err.(fs.PathError)</span>; ok { fmt.Printf("PathError: main: %v\n", e.Err) } </pre> <br /> 대신 내부 속성을 접근해 재차 묻는 형태로 코딩해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > if e, ok := <span style='color: blue; font-weight: bold'>err.(WrapError)</span>; ok { if inner, ok := <span style='color: blue; font-weight: bold'>e.Err.(*fs.PathError)</span>; ok { fmt.Printf("%v\n", inner) } } </pre> <br /> 일단 해결을 한 듯 보이지만, 사실 우리가 원하는 종류의 오류 타입이 다시 그 하위에, 또다시 그 하위에 감싸인 것인지 알 수 없으므로 완벽한 해결책은 아닙니다. 바로 이런 문제 때문에 이후 설명할 errors.Is, errors.As가 나옵니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 위의 경우 우리가 WrapError를 구현했는데요, Go 언어 측에는 이미 이 용도로 (private 접근이지만) fmt.wrapError라는 타입이 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > /* // 명시적으로는 없고 필요할 때만 인라인 정의 interface { Unwrap() error } // builtin.go에 정의 type error interface { Error() string } */ <span style='color: blue; font-weight: bold'>type wrapError struct { msg string err error }</span> func (e *wrapError) <span style='color: blue; font-weight: bold'>Error()</span> string { <span style='color: blue; font-weight: bold'>return e.msg</span> } func (e *wrapError) <span style='color: blue; font-weight: bold'>Unwrap()</span> error { <span style='color: blue; font-weight: bold'>return e.err</span> } </pre> <br /> 보는 바와 같이 내부 필드 2개가 모두 private 접근이고, 대신 error 인터페이스의 함수인 Error()를 통해 msg를 반환하고, 필요할 때마다 Unwrap을 포함한 인라인 인터페이스를 조회해 내부 err 필드를 접근할 수 있습니다.<br /> <br /> 그리고, 저렇게 기존 에러와 새로운 에러 메시지를 합쳐 fmt.wrapError를 반환해 주는 함수가 "%w" 인자를 이용한 fmt.Errorf입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > file, err := os.Open("test.dat") if err != nil { <span style='color: blue; font-weight: bold'>wrapped := fmt.Errorf("doWork failed: %w", err)</span> fmt.Printf("wrapped: %v, %T\n", wrapped, wrapped) /* 출력 결과 wrapped: doWork failed: open test.dat: The system cannot find the file specified., *fmt.wrapError */ } </pre> <br /> fmt.Errorf는 (msg == "doWork failed: %w", err), (err == err)로 설정된 fmt.wrapError 인스턴스를 반환하고, 이런 경우 다음과 같이 errors.Is 함수를 사용해 (값에 대한) 비교를 하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > if <span style='color: blue; font-weight: bold'>errors.Is(wrapped, err)</span> { // true 평가 fmt.Printf("errors.Is Error %v\n", wrapped) } </pre> <br /> 2개의 error 개체가 같다고 판정을 합니다. 그런데 이상하지 않나요? Errorf가 반환한 fmt.wrapError가 왜 fs.PathError와 같은 걸까요? 왜냐하면, errors.Is는 내부적으로 Unwrap을 호출하는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // wrap.go에 errors.Is, Unwrap 정의 // errors.Is가 호출하는 Unwrap 함수 func Unwrap(err error) error { u, ok := err.(<span style='color: blue; font-weight: bold'>interface { Unwrap() error }</span>) if !ok { return nil } <span style='color: blue; font-weight: bold'>return u.Unwrap()</span> } func Is(err, target error) bool { if target == nil { return err == target } isComparable := reflectlite.TypeOf(target).Comparable() <span style='color: blue; font-weight: bold'>for {</span>} if isComparable && err == target { return true } if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { return true } // TODO: consider supporting target.Is(err). This would allow // user-definable predicates, but also may allow for coping with sloppy // APIs, thereby making it easier to get away with them. <span style='color: blue; font-weight: bold'>if err = Unwrap(err); err == nil { return false }</span> } } </pre> <br /> 보는 바와 같이 해당 개체가 interface { Unwrap() error }을 구현하고 있는지 확인하므로 결국 fmt.wrapError의 Unwrap 함수가 불려 내부에 감싸진 에러와 비교를 하기 때문입니다.<br /> <br /> 위와 같은 동작 방식으로 인해 실수할 여지가 하나 있습니다. 개발자가 임의로 만든 error 타입이 있다면 반드시 Unwrap도 구현해줘야만 errors.Is(및 errors.As) 코드가 정상적으로 동작할 수 있다는 점입니다. (물론 에러 간의 연결 고리를 유지하지 않을 error 타입이라면 저런 고려 자체가 의미가 없습니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> errors.Is의 Unwrap 동작은 errors.As에서도 동일하게 발생합니다. 따라서, fmt.Errorf를 이용해 panic 처리한 오류라면 recover로 반환한 에러 개체에 대해 다음과 같이 직접 오류 타입으로의 형변환 가능성을 테스트할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > func main() { defer func() { if e := recover(); e != nil { var pathError *fs.PathError // fs.PathError인 경우에만 처리하도록 분기 (타입에 해당한다면 그 형식으로 변환한 인스턴스를 반환) <span style='color: blue; font-weight: bold'>if errors.As(e.(error), &pathError)</span> { fmt.Printf("errors.As: %v\n", e) } } }() doWork() } func doWork() { file, err := os.Open("test.dat") if err != nil { <span style='color: blue; font-weight: bold'>panic(fmt.Errorf("doWork failed: %w", err))</span> } defer file.Close() } </pre> <br /> 하지만, 이게 범용적으로 쓸 수 있는 defer 루틴이 아닙니다. 일례로 다음과 같이 코드를 바꾸면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > func doWork() { // ...[생략]... <span style='color: blue; font-weight: bold'>panic("Test")</span> } </pre> <br /> errors.As에서 fatalpanic으로 빠지는 것을 볼 수 있습니다. 왜냐하면 (recover는 "interface {}" 타입을 반환하고) errors.As의 첫 번째 인자는 반드시 error 타입이어야 하기 때문입니다. 그래서 defer의 코드가 이렇게 바뀌어야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > defer func() { if e := recover(); e != nil { <span style='color: blue; font-weight: bold'>if err, ok := e.(error); ok {</span> var pathError *fs.PathError if <span style='color: blue; font-weight: bold'>errors.As(err, &pathError)</span> { fmt.Printf("errors.As: %v\n", e) } } } }() </pre> <br /> 이와 관련해 실수할 수 있는 또 다른 사용법이 바로 log.go의 log.Panic을 다음과 같은 식으로 사용하는 경우입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > file, err := os.Open("test.dat") if err != nil { <span style='color: blue; font-weight: bold'>log.Panic(fmt.Errorf("failed to send data to data server: %w", err))</span> // 위의 코드는 사실상 아래의 코드와 동일합니다. // log.Panicf("failed to send data to data server: ... %v", err.Error()) } </pre> <br /> 왜냐하면 log.Panic의 내부 구현이,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > func Panic(v ...interface{}) { <span style='color: blue; font-weight: bold'>s := fmt.Sprint</span>(v...) std.Output(2, s) <span style='color: blue; font-weight: bold'>panic(s)</span> } </pre> <br /> 결국 문자열 반환이기 때문에 fmt.Errorf를 사용하는 의미가 없습니다. 즉, log.Panic을 사용하면 위에서 만든 errors.As + fs.PathError 테스트 코드가 동작하지 않습니다. (log.Panic + fmt.Errorf의 원래 의도대로라면 panic + fmt.Errorf로 해야 맞습니다.) 왠지... Go 언어의 예외 처리 방식이 산만한다고 느껴지지 않나요? ^^;<br /> <br /> <hr style='width: 50%' /><br /> <br /> 마지막으로, 그나마 마음에 드는 콜스택을 알아보겠습니다. 이것은 그냥 debug.Stack()을 사용하면 끝입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > defer func() { if e := recover(); e != nil { fmt.Printf("error: %v, %s\n", e, <span style='color: blue; font-weight: bold'>debug.Stack()</span>) } }() </pre> <br /> 재미있는 건, panic을 호출 스택에서 중첩시킨 경우입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > func do2Work() { defer func() { if e := recover(); e != nil { <span style='color: blue; font-weight: bold'>panic("failed")</span> } }() doWork() } func doWork() { file, err := os.Open("test.dat") if err != nil { wrapped := fmt.Errorf("doWork failed: %w", err) <span style='color: blue; font-weight: bold'>panic(wrapped)</span> } defer file.Close() } </pre> <br /> 별다르게 panic 정보를 연결하지도 않았는데, 마지막의 debug.Stack()에서는 중간에 panic("failed")을 호출한 것에 상관없이 전체 호출 스택 정보를 보여줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > runtime/debug.Stack() C:/Go/src/runtime/debug/stack.go:24 +0x65 main.main.func1() C:/gowork/work/panicTest/main.go:19 +0x39 panic({0xbc7c40, 0xbf9aa0}) C:/Go/src/runtime/panic.go:1038 +0x215 main.do2Work.func1() C:/gowork/work/panicTest/main.go:118 +0x45 panic({0xbcb0c0, 0xc0000a4400}) C:/Go/src/runtime/panic.go:1038 +0x215 <span style='color: blue; font-weight: bold'>main.doWork() C:/gowork/work/panicTest/main.go:140 +0xc5 main.do2Work() C:/gowork/work/panicTest/main.go:122 +0x3b</span> main.main() C:/gowork/work/panicTest/main.go:90 +0x3b </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
9907
(왼쪽의 숫자를 입력해야 합니다.)