![Golang Convention 중 논의를 해야 할 사항 정리](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwiakk%2FbtsKD3DIujO%2FXWzM938QmV6ViSX0iNw1Qk%2Fimg.png)
1. 에러 핸들링
Error는 체크하지 않고 gracefully handle한다.
Golang에서는 아래와 같이 작성 하는게 자연스러워 보이지만 사실은 불필요합니다.
Bad
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return err
}
return nil
}
Good
return authenticate(r.User)
오류에 주석을 달아라 (Annotating Errors)
github.com/pkg/errors 패키지는 과거에 널리 사용되었지만, Go 1.13부터는 Go 표준 라이브러리에 errors 패키지에 기본적으로 오류를 래핑하고 원인을 추적하는 기능이 추가되었습니다. 최신 Go에서는 fmt.Errorf 및 errors의 Unwrap, Is, As 함수를 사용하여 비슷한 기능을 구현할 수 있습니다.
주요 함수 설명
- fmt.Errorf: "%w" 포맷 지시자를 사용하여 오류를 래핑할 수 있습니다.
- errors.Unwrap: 래핑된 오류에서 원래 오류를 추출할 수 있습니다.
- errors.Is 및 errors.As`: 특정 오류 타입이나 값을 검사할 때 사용됩니다.
예제 코드
package main
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open failed: %w", err)
}
defer f.Close()
buf, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read failed: %w", err)
}
return buf, nil
}
func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
if err != nil {
return nil, fmt.Errorf("could not read config: %w", err)
}
return config, nil
}
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Println(err) // 기본 출력
os.Exit(1)
}
}
출력 결과
// could not read config: open failed: open /Users/username/.settings.xml: no such file or directory
또한, 이는 unwrap을 통해서 하나하나씩 에러 처리도 가능하니 참고 바랍니다.
Unwrap 예시
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// 래핑된 오류 생성
originalErr := os.ErrNotExist
wrappedErr := fmt.Errorf("could not open file: %w", originalErr)
// 래핑된 오류 출력
fmt.Println("Wrapped error:", wrappedErr)
// 원래 오류 추출
unwrappedErr := errors.Unwrap(wrappedErr)
fmt.Println("Unwrapped error:", unwrappedErr)
}
////
출력 결과
Wrapped error: could not open file: file does not exist
Unwrapped error: file does not exist
에러는 한번만 처리하라
에러 로그가 여러번 찍히는 것 보다 한번에 순서대로 다 찍히는게 좋습니다.
Bad
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
// 주석이 달린 에러가 로그 파일에 찍힙니다.
log.Println("unable to write:", err)
// 주석이 달리지 않은 에러가 Caller에게 전달됩니다.
return err
}
return nil
}
Good
func Write(w io.Write, buf []byte) error {
_, err := w.Write(buf)
return errors.Wrap(err, "write failed")
}
설명을 추가한 에러를 위로 올려서 Caller가 처리하도록 합시다.
2. 에러 로깅 방식
불필요한 문구는 삭제하기
불필요한 문구인 Failed to 나 error occured라는 내용은 추가하지 않습니다.
이는 가독성을 떨어뜨리게 됩니다.
Bad
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %s", err)
}
// failed to x: failed to y: failed to create new store: the error
Good
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %s", err)
}
// x: y: new store: the error
에러 로깅 원칙
위의 에러 Annotation 원칙에 따라서 에러는 최상위 레벨에서만 한번 찍어줍니다.
그러면 아래와 같은 장점을 얻을 수 있습니다.
- 중복 로그 방지: 여러 레벨에서 에러 로그를 출력하면 동일한 에러가 중복으로 출력될 수 있어, 로그가 불필요하게 복잡해집니다.
- 일관된 에러 핸들링: 프로그램의 최상위 레벨에서 에러를 한 번만 출력함으로써, 모든 에러가 동일한 형식으로 기록되거나 출력됩니다. 이를 통해 로그의 일관성을 유지할 수 있습니다.
- 스택 추적: 에러를 래핑하면서 메시지를 추가하면, 최종적으로 가장 상위 레벨에서 출력할 때 에러 체인에 대한 충분한 정보를 제공할 수 있습니다.
func main() {
_, err := ReadConfig()
if err != nil {
// 최상위 레벨에서 에러 출력
fmt.Println("Error:", err)
os.Exit(1)
}
}
////
출력 예시
Error: could not read config: open failed: open /Users/username/.settings.xml: no such file or directory
3. 전역 변수 사용
변경 가능한(mutable) 전역변수 피하기
변경 가능한(mutable) 전역 변수를 피하고, 대신 의존성 주입을 선택한다.
이 사항은 함수 포인터 뿐 만 아니라 다른 종류의 값에도 적용된다.
즉, 대부분의 경우에는 전역 변수 사용을 피하라.
Bad
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
Good
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
Reference
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
'백엔드 > Golang' 카테고리의 다른 글
Golang Convention : Error wrapping vs Opaque error (0) | 2024.11.17 |
---|---|
Golang 에러 처리 - (1) Google Guide/Best Practice 찾아보기 (2) | 2024.09.07 |
Golang zero-value 알아보기 (0) | 2024.07.09 |
[번역] Golang vs Spring boot native 성능 비교해보기 - Hello world 케이스 (0) | 2024.07.08 |
Golang init() 사용법 및 주의 사항 (0) | 2024.05.16 |
개발 및 IT 관련 포스팅을 작성 하는 블로그입니다.
IT 기술 및 개인 개발에 대한 내용을 작성하는 블로그입니다. 많은 분들과 소통하며 의견을 나누고 싶습니다.