![Golang 에러 처리 - (2) 에러 처리 시 결정해야 할 항목들](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYkaW8%2FbtsJuE0e6qA%2FTw5aDEqDlixfzNOf69kKc1%2Fimg.jpg)
![](https://blog.kakaocdn.net/dn/cYkaW8/btsJuE0e6qA/Tw5aDEqDlixfzNOf69kKc1/img.jpg)
Golang Error 처리를 위한 결정
1. Error Wrapping으로 추가 정보를 포함한다.
Wrapping 방법은 fmt.Errorf(”%w”, err)를 사용하여 에러를 추가하는 방식입니다.
Go 1.13 이상 버전에서는 표준 라이브러리에 error 래핑 기능이 추가되어, 호출 스택이 필요 없는 경우에는 다음과 같이 간편하게 사용할 수 있습니다.
func foo() error {
return errors.New("foo error!!")
}
func bar() error {
return fmt.Errorf("%s, %w", "bar", foo())
}
func main() {
err := fmt.Errorf("%s, %w", "main!!", bar())
fmt.Printf("%+v", err)
}
string formatting 방식과 비슷하며, %w를 사용하여 message가 아니라 error를 직접 래핑할 수 있습니다.
그리고 마지막에 %+v로 err를 출력하면, 래핑된 error들이 출력됩니다.
main!!, bar, foo error!!
간략한 사용방법은 위와 같으며 추천하는 방식은 아래와 같습니다.
아래와 같은 포맷으로 작성하면 함수의 호출 흐름과 에러 표시의 흐름이 동일하여 좋습니다.
// Good:
err1 := fmt.Errorf("err1")
err2 := fmt.Errorf("err2: %w", err1)
err3 := fmt.Errorf("err3: %w", err2)
fmt.Println(err3) // err3: err2: err1
// err3은 new->old 체인을 가지며, new->old 순서로 에러를 출력합니다.
다만, Wrapping시에 Error 메세지 작성은 조심해야합니다.
failed...나 error occured while... 같은 문구를 접두사로 붙이면 노이즈가 될 가능성이 높으며, 가독성이 떨어집니다.
Wrap시에 이런 메시지들이 반복될 가능성이 높기 때문입니다.
// BAD
if err != nil {
return fmt.Errorf("failed to create new store:%w", err)
}
// output : failed to x: failed to y: failed to create new store: the error
// GOOD
if err != nil {
return fmt.Errorf("new store : %w", err)
}
// output : x: y: new store: the error
github.com/pkg/errors 를 활용해서 wrapping하는 방식도 있습니다. 참고바랍니다.
2. Custom Error에 대한 고민
Uber guide를 통해서 조금 더 디테일한 Error 처리에 대한 가이드를 확인할 수 있었습니다.
에러 처리에 있어서 4가지 정도의 옵션을 제시했는데요.
1. errors.New 간단한 정적 문자열(simple static strings)과 함께하는 에러
2. fmt.Errorf 형식화된 오류 문자열
3. Error() 메서드를 구현한 커스텀 타입 (Custom types)
4. "pkg/errors".Wrap를 사용하여 래핑 된(wrapped) 오류
각 옵션에 대해서 아래 항목을 고려하라고 되어있습니다.
1. 추가 정보가 필요없는 간단한 에러인가? 그렇다면, errors.New가 충분하다.
3. 클라이언트가 오류를 감지하고 처리(handle)해야 하는가? 그렇다면, 커스텀 타입을 사용해야 하고 Error() 메서드를 구현해야 한다.
4. 다운스트림 함수(downstream function)에 의해 반환된 에러를 전파(propagating)하고 있는가? 그렇다면, Error Wrapping을 참고하라.
2. 이외의 경우, fmt.Errorf 로 충분하다.
Custom Type의 best practice의 예시
// BAD
func open(file string) error {
return fmt.Errorf("file %q not found", file)
}
func use() {
if err := open(); err != nil {
if strings.Contains(err.Error(), "not found") {
// handle
} else {
panic("unknown error")
}
}
}
//GOOD
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func open(file string) error {
return errNotFound{file: file}
}
func use() {
if err := open(); err != nil {
if _, ok := err.(errNotFound); ok {
// handle
} else {
panic("unknown error")
}
}
}
몇 가지 프로젝트 사례를 봤을때 커스텀 에러 메세지에는 에러 코드와 내용을 포함 하는 경우도 종종 있었습니다.
또한, 개인적인 판단으로는 Internal Error 처리(로깅용)와 External Error (Error를 사용자에게 알려주기)
도 의미가 있겠다 라는 생각을 했었습니다. 아래와 같이 말이죠.
type ExternalErr struct {
// sent to the frontend
ErrStatus int `json:"status"` // HTTP Status Code
ErrTitle string `json:"title"` // The string representation of the Status Code like "bad_request"
ErrMessage string `json:"message"` // An optional message send to the front_end
}
type InternalErr struct {
// never sent to the frontend, only used for logging
ErrError error `json:"error"` // Raw error returned by a DB, another Servive or whatever
ErrErrorMsg string `json:"error_msg"` // String representation of ErrError
ErrCode string `json:"code"` // Raw error code from the DB or another service
}
type RestErr struct {
ExternalErr
InternalErr
}
그러나 전체적으로 Erorr handling에 대해서 고민 해 봤을때 이렇게 나눌만큼 의미있는 Error 분리가 될지 오히려 개발자들이 에러처리에 더 혼란을 겪는게 아닌지 이런 생각도 들었습니다.
또한, Error type을 만들어서 추가적인 Context를 포함하는것은 취향의 영역인것 같기도합니다.ㅠ
Custom Error 생성을 반대하는 글도 있으니 참고해보시면 좋을것 같습니다. (아래 글의 Error types를 참고)
Don’t just check errors, handle them gracefully | Dave Cheney
Don’t just check errors, handle them gracefully | Dave Cheney
This post is an extract from my presentation at the recent GoCon spring conference in Tokyo, Japan. Errors are just values I’ve spent a lot of time thinking about the best way to handle errors in Go programs. I really wanted there to be a single way to
dave.cheney.net
3. Error는 반드시 한번만 처리한다.
결국은 Wrapping 방식을 쓰면 parent에게 맡기는게 가장 좋은 방법이라는 것을 이전 포스트에서 확인 할 수 있었습니다.
Golang 에러 처리 - (1) Google Guide/Best Practice 찾아보기 (tistory.com)
Golang 에러 처리 - (1) Google Guide/Best Practice 찾아보기
배경현재 프로젝트에서 Error 처리를 일관적이지 못한 방법으로 하고 있다는 생각이 들어서 글을 쓰게 되었습니다.작년의 Golang 초기 사용 시점에는 모두 언어에 대한 이해도가 낮고 MVP 완성을 위
ray5273.tistory.com
따라서, 에러 처리는 함수의 Caller에서 단 한번만 처리하는게 좋습니다.
에러 로깅 또한 Caller에서 해주는게 좋습니다.
// BAD
if err := nil {
log.Println("failed to get user id: %d", userID)
return err
}
// GOOD
if err := nil {
return err
}
4. Defer 사용시에도 Error 처리를 잊지 말자
defer 처리된 Close 메서드에서 반환된 error를 확인하는 것을 잊어버릴 때가 있습니다.
f, err := os.Open(...)
if err != nil {
// handle..
}
defer f.Close() // 여기에서 에러가 발생한다면?
defer에서 에러가 발생하는 경우 중대한 버그를 유발 할 수 있습니다.
따라서, defer를 사용하는 경우에도 항상 error를 handle해야합니다.
아래와 같이 특정 wrapper를 사용해서 defer시에도 Error 처리를 하게 할 수 있습니다.;
// Use `CloseWithErrCapture` if you want to close and fail the function or
// method on a `f.Close` error (make sure thr `error` return argument is
// named as `err`). If the error is already present, `CloseWithErrCapture`
// will append (not wrap) the `f.Close` error if any.
defer runutil.CloseWithErrCapture(&err, f, "close file")
// Use `CloseWithLogOnErr` if you want to close and log error on `Warn`
// level on a `f.Close` error.
defer runutil.CloseWithLogOnErr(logger, f, "close file")
또한, 다른 방법도 있습니다.
위와 같은 wrapper 패키지가 우리의 구조에 맞지 않거나 사용하기 꺼려진다면 errors.Join()을 사용할 수 있습니다.
쉽게 defer func()를 사용하면 아래와 같이 사용할 수 있습니다.
func example(r io.ReadCloser) (err error) {
// We need this big anonymous function that takes up a bunch of
// precious lines...
defer func() {
// Ugghh, need to check for the error and then set it D:
cerr := r.Close()
if cerr != nil {
err = cerr
}
}
// ... code that reads from r ...
}
다만, 이렇게 쓰는 경우
error 1 : example()함수에서 에러가 나고
error 2 : defer() 에서도 에러가 나면
원래의 에러를 defer()의 에러가 덮어써버려 error1만 err 변수에 할당되게 됩니다.
이를 해결하기 위해서 errors.join()을 사용하여 두 에러를 모두 error 변수에 담을 수 있습니다.
func example(r io.ReadCloser) (err error) {
defer func() {
err = errors.Join(err, r.Close()) // Magic!
}()
}
위와 같이; 심플하게 코드를 작성하고 error까지 처리했습니다
5. Goroutine의 Error 처리
errgroup으로 goroutine 10배 잘 활용하기 | DevJin-Blog
errgroup으로 goroutine 10배 잘 활용하기
goroutine과 channel은 Golang을 공부할 때 가장 많이 접하게 되는 용어들이다. 그리고 goroutine과 channel을 통해서 동시성 프로그래밍을 사용할 수 있게 된다. 이번 블로그 포스트에서는 goroutine을 1…
devjin-blog.com
Reference
GitHub - zzerjae/thanos-coding-style-guide-kr: Thanos Coding Style Guide Translation in Korean
Thanos Coding Style Guide Translation in Korean. Contribute to zzerjae/thanos-coding-style-guide-kr development by creating an account on GitHub.
github.com
Error는 검사만 하지말고, 우아하게 처리하세요. - Rain.i
All about IT tech, especially database, cloud, linux, clustering.
cloudrain21.com
errors.Join ❤️ defer
A common gripe I’ve had with Go is that the mantra is “you should handle errors”, but at the same time the ergonomics of handling errors from (io.ReadCloser).Close() in a defer call is cumbersome. But fear no more! With the Go 1.20 release, there’s
wstrm.dev
GitHub - TangoEnSkai/uber-go-style-guide-kr: Uber's Go Style Guide Official Translation in Korean. Linked to the uber-go/guide a
Uber's Go Style Guide Official Translation in Korean. Linked to the uber-go/guide as a part of contributions - TangoEnSkai/uber-go-style-guide-kr
github.com
뱅크샐러드 Go 코딩 컨벤션 | 뱅크샐러드
안녕하세요, 뱅크샐러드 코어 백엔드 팀의 정겨울입니다. 뱅크샐러드는 백엔드 서비스에 다양한 언어를 사용하고 있습니다. 특히 지난 4년간은 Go와 gRPC…
blog.banksalad.com
개발 및 IT 관련 포스팅을 작성 하는 블로그입니다.
IT 기술 및 개인 개발에 대한 내용을 작성하는 블로그입니다. 많은 분들과 소통하며 의견을 나누고 싶습니다.