본문 바로가기
Dev/Go

Go 코딩 컨벤션

by Sovereign 2024. 7. 11.

 

프로젝트를 진행할 때 아래와 같은 코딩 컨벤션을 지키려 합니다.

 

원본 출처뱅크샐러드 Go 코딩 컨벤션 | 뱅크샐러드 (banksalad.com)


# 1, Don't Panic

프로덕션 환경에서 서버가 올바르게 시작되고 요청을 처리할 수 있는 상태에선 절대 panic을 사용하지 않는다. 또한 프로세스를 종료시키는 fatal도 마찬가지로 사용하지 않는다. panic은 다른 언어의 try-catch 문법처럼 예외처리를 위한 것이 아니며, 서버 애플리케이션 초기화 시점에만 빠른 실패를 위해 사용한다. 의도치 않은 panic에 대비해 서버 인터셉터 혹은 미들웨어로 recovery 체인을 추가하는 것을 권장한다.  (e.g. echo의 미들웨어, grpc 인터셉터)

 

 

# 2, Panic을 낼 수 있는 함수는 must prefix 붙이기

내부에서 panic이 발생할 수 있는 함수에는 'must' prefix를 붙이는 것이 관례다.  (e.g. regexp.MustCompile(), netip.MustParseAddr()). 이러한 함수들은 주로 초기화 시점에서만 사용되어야 한다.

 

 

# 3, Panic vs Fatal

panic은 스택트레이스와 함께 stderr에 출력되며, deferred 함수를 실행한다. 반면, fatal은 트레이스 없이 현재 설정된 output에 출력되며, os.Exit(1)을 수행한다.

 

초기화 시점에서만 사용되며 스택 출력이 간결한 fatal이 유용할 때도 있고, 테스트 코드에서는 스택트레이스를 위해 panic을 사용하기도 한다.

 

 

# 4, Panic safe goroutine

고루틴을 사용할 때, 에러 처리를 좀 더 편하게 하기 위해 sync.Waitgroup 또는 x/sync/errgroup 을 사용한다. 그러나 errgroup은 하나라도 에러가 발생하면 다른 고루틴에 cancel이 전파되거나 하나의 에러만 기록되는 제한이 있어, 멀티 에러 고루틴을 위해 hashicorp/go-multierror 를 사용한다.

 

또한, 핸들러 안에서 panic safe 한 코드를 작성하기 위해 별도의 grouper를 만들어 사용한다.

 

 

# 5, Concurrent safe한 결과 모으기

여러 고루틴의 결과를 한 곳으로 모아야 할 때, 직관적으로 떠오르는 slice append를 사용하면 data race 에러가 발생할 수 있다. 이를 해결하기 위해 mutex를 사용하거나, sync.Map 또는 채널을 사용하는 것이 좋다.

 

 

# 6, Error stacking

에러 스택 트레이스를 추가하기 위해 pkg/errors 패키지를 사용한다. errors.WithStack(err)로 해당 에러가 발생한 시점의 스택 트레이스를 덧붙일 수 있다. 이런 stacking은 가장 안쪽에서 한 번만 하는 것이 원칙이다.

 

 

# 7, Error logging

핸들러 내부에서 발생한 에러는 미들웨어에 의해 로깅하는 것을 권장한다. defer에서 발생한 에러는 무시하지 않고 에러 레벨로 로깅해야 한다. 에러 로깅은 가능한 parent에게 맡기는 것이 원칙이다.



  • 에러 로깅은 parent에게 맡기는 게 원칙

 

 

# 8, No named return

함수 시작 시 기본 값으로 초기화 되는 변수라고 이해하면 좋다. 같은 유형이 반환될 때 어떤 의미의 값인지 인지하기 쉬워진다. 하지만 함수가 길어질수록 언제 어느 값으로 리턴되는지 추적이 어려우므로 일관되게 사용하지 않는다.

 

 

# 9, Slice 선언 시 len, cap 설정

slice에 추가될 아이템의 개수를 미리 알고 있다면 len을, 적어도 cap을 설정하는 것이 좋다. map도 마찬가지로 cap을 설정하는 것을 권장한다. 이는 가독성을 크게 저하시키지 않으며, 성능 향상에 도움이 된다.

 

 

# 10, Nil slice vs Empty slice

언제나 nil slice를 사용하는 것을 권장한다. slice, map의 emptyness는 항상 len()을 사용해 판단해야 한다.

 

 

# 11, Bool map과 struct{} map

유니크함을 따질 때나 이미 존재하는지 체크하기 위해 slice를 map으로 만들 때, map[type]bool 또는 map[type]struct{} 두 가지 중 선택할 수 있다. struct{} map이 메모리 효율적인 경향이 있지만, bool map이 약간 더 간결한 편이라 기호에 따라 선택할 수 있다.

 

 

# 12, Map 조회 시 ok 체크

v := m[k] 보단 v, ok := m[k]를 사용하는 것을 권장한다. 반드시 있어야 하는 값이 아닌 경우 방어적으로 짜는 것이 유용하다. 다만 실제로 언제나 있을 수밖에 없는 경우 굳이 if !ok { return err }을 사용하지 않아도 된다.

 

 

Avoid map loop

Go는 map의 순회 순서를 보장하지 않는다. map을 순회하며 slice나 기타 반환 값을 만들면 flaky 테스트를 유발하거나 해시 값이 달라지는 버그가 발생할 수 있다. append 후 sort를 하거나 아예 map을 순회하지 않는 것을 권장한다.

 

 

# 13, 문자열 loop 시 range 사용

문자열을 순회할 때 "for i := 0; i < len(s); i++" 처럼 하면 안 되고 "for i, rune := range s" 를 사용해야 한다.

 

 

# 14, 문자열 길이

문자열의 길이를 알아내기 위해서는 "unicode/utf8" 패키지의 "utf8.RuneCountInString" 을 사용해야 한다.

 

 

# 15, Early return 사용

nested 구조보다는 flatten한 구조를 선호한다. 여러 validation이 있다면 main flow는 해피 케이스가 되도록 구성한다.

 

 

# 16, time.Duration 사용

"const timeoutInSeconds = 5" 보다는 "const timeout = 5 * time.Second" 처럼 사용하는 것이 좋다. 함수 인자로는 언제나 time.Duration을 사용한다. 환경변수는 "os.Getenv("TIMEOUT_IN_SECONDS")"로 받되 가능한 한 빨리 "time.Duration"으로 변환하는 것을 권장한다.

 

 

# 17, 타임존

time.Now()는 로컬 타임존을 반영한 값을 반환한다. UTC()를 붙이거나 .In(KST)를 붙여 사용하는 것이 좋다. 초기화 단계에서 미리 로딩한 타임존을 활용하는 것을 권장한다.

 

 

# 18, 테이블 기반 테스트

가능한 한 "given, when, then" 구조의 테이블 기반 테스트를 사용한다. stretchr/testify 라이브러리의 각종 assert, require 패키지를 적극 사용한다.

 

 

# 19, No monkey patch

사이드 이펙트가 있는 코드는 항상 외부에서 주입하는 것을 권장한다. 핸들러 내부에서 "time.Now()", "rand.Int()"를 직접 호출하기보다는 "nowFunc func() time.Time()", "rander func() int"를 인자로 넘긴다.

 

 

# 20, Avoid reflect

reflect를 사용해야만 하는 경우가 아니면 사용하지 않는 것이 좋다. 제너레이터나 테스트 코드에서는 유용할 수 있지만, 서버 핸들러에서는 무언가 잘못된 신호일 수 있다.

 

 

# 21, Functional options

Go에는 optional parameter가 없다. 필수 파라미터를 분리하고 기본 값을 설정하거나 함수 시그니쳐의 하위 호환성을 보장하기 위해 라이브러리에 권장되는 패턴을 사용한다.

 


출처

뱅크샐러드 Go 코딩 컨벤션 | 뱅크샐러드 (banksalad.com)

 

뱅크샐러드 Go 코딩 컨벤션 | 뱅크샐러드

안녕하세요, 뱅크샐러드 코어 백엔드 팀의 정겨울입니다. 뱅크샐러드는 백엔드 서비스에 다양한 언어를 사용하고 있습니다. 특히 지난 4년간은 Go와 gRPC…

blog.banksalad.com

https://go.dev/doc/effective_go

 

Effective Go - The Go Programming Language

Documentation Effective Go Effective Go Introduction Go is a new language. Although it borrows ideas from existing languages, it has unusual properties that make effective Go programs different in character from programs written in its relatives. A straigh

go.dev

TangoEnSkai/uber-go-style-guide-kr: Uber's Go Style Guide Official Translation in Korean. Linked to the uber-go/guide as a part of contributions (github.com)

 

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

https://google.github.io/styleguide/go/best-practices.html

 

styleguide

Style guides for Google-originated open-source projects

google.github.io

https://jacking75.github.io/go-20220227/

 

golang - go에서 에러 핸들링을 무난하게 하는 방법 (2021.07 현재) - jacking75

golang - go에서 에러 핸들링을 무난하게 하는 방법 (2021.07 현재) 이 글은 2022-02-27에 작성되었습니다. Tagged: go golang error

jacking75.github.io

Error handling and Go | go.dev

 

Error handling and Go - The Go Programming Language

Error handling and Go Andrew Gerrand 12 July 2011 Introduction If you have written any Go code you have probably encountered the built-in error type. Go code uses error values to indicate an abnormal state. For example, the os.Open function returns a non-n

go.dev

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