전 직장에서 go 언어를 처음 쓰기 시작해 현 직장에서도 계속 쓰며 약 5년간 활발히 사용하고 있다.
예전과 비교하면 go 생태계는 점점 풍부해지고, 고퍼콘 코리아를 비롯한 다양한 커뮤니티도 늘어나고, 레퍼런스도 많아지고 있다.
그럼에도 여전히 (비단 go 언어만 그런 건 아니지만) 잘 정리된 한국어 자료는 적고 시행착오를 겪어봐야 하는 부분이 많아 그동안 여러 동료와 함께 개발하며 겪었던 시행착오와 권장 사항을 프로덕션 환경에서 사용하는 golang과 gRPC, 뱅크샐러드 Go 코딩 컨벤션 두 편의 글로 정리하기도 했다.
그와 비슷한 결로 이번엔 주로 go 언어로 서버 개발을 하며 자주 사용하고 유용했던 패키지라이브러리를 정리해 봤다.
go 언어에서 기본으로 제공하는 내장 패키지에도 net/http/httptest
, crypto/rand
패키지처럼 믿고 쓸 수 있고 잘 만들어진 패키지는 많으나 이 글에선 서드 파티 패키지에 집중했다.
글을 쓸 때 마다 항상 드는 고민이 '이 정도면 "golang xxx package" 같은 키워드로 검색해도 금방 나올 텐데 괜히 적는 거 아닌가?'라는 부분이다. 그럼에도 누군가의 의사결정 비용과 고민을 줄여주길 바라며, 또 이 글을 읽은 다양한 사람들이 얹어주는 한마디씩을 통해 이 글이 더 발전되길 바라며 소개해 본다.
func TestSomething(t *testing.T) {
assert.Equal(t, expected, got, "they should be equal")
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.JSONEq(t, `{"name": "hello"}`, msg)
}
A toolkit with common assertions and mocks that plays nicely with the standard library
if got != expected { t.Errorf("...") }
대신 assert.Equal(t, expected, got)
코드로 쓸 수 있어 좀 더 읽기 쉬워지는 측면도 있다.import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"
)
func main() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
// ...
log.Error().Stack().Err(err).Msg("failed to insert row to db")
}
rs/zerolog: Zero Allocation JSON Logger
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
}).Info("A walrus appears")
}
sirupsen/logrus: Structured, pluggable logging for Go.
WithFields
메서드 때문에 zerolog 대비 사용성이 좋았다.log/slog
도 사용해볼만한 대안이다.
resp, err := userSvc.GetUser(ctx, &GetUserRequest{...})
if err != nil {
return errors.Wrap(err, "user.GetUser")
}
Simple error handling primitives
errors
표준 라이브러리의 기능 부족으로 생겼으나 Unwrap, Is, As가 편입된 이후에도 de-facto로 사용된다.var errs *multierror.Error
if err := step1(); err != nil {
errs = multierror.Append(errs, err)
}
if err := step2(); err != nil {
errs = multierror.Append(errs, err)
}
return errs.ErrorOrNil()
// Output:
// 2 errors occurred:
// * error 1
// * error 2
A Go (golang) package for representing a list of errors as a single error.
multierror.Group
도 고루틴을 fanout해 실행하고 각각의 에러를 취합해야 할 때 사용하곤한다.names := lo.Uniq([]string{"Samuel", "John", "Samuel"})
// []string{"Samuel", "John"}
A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)
Map
, SliceToMap
, Keys
함수를 많이 사용했다.
func main() {
price, err := decimal.NewFromString("136.02")
quantity := decimal.NewFromInt(3)
subtotal := price.Mul(quantity)
fmt.Println("Subtotal:", subtotal) // Subtotal: 408.06
}
Arbitrary-precision fixed-point decimal numbers in Go
shopspring/decimal
가 훨씬 좋았다.func main() {
cache, _ := ristretto.NewCache(&ristretto.Config[string,string]{
NumCounters: 1e7, // number of keys to track frequency of (10M).
MaxCost: 1 << 30, // maximum cost of cache (1GB).
BufferItems: 64, // number of keys per Get buffer.
})
cache.Set("key", "value", 1)
value, found := cache.Get("key")
fmt.Println(value)
}
A high performance memory-bound Go cache
import (
"github.com/volatiletech/sqlboiler/v4"
)
func main() {
users, err := model.Users().All(ctx, db)
token, err := model.Tokens(model.TokenWhere.AccessToken.EQ(accessToken)).One(ctx, db)
}
token.Update(ctx, db, boil.Whitelist(
model.TokenColumns.AccessToken,
model.TokenColumns.AccessTokenExpiredAt,
))
Generate a Go ORM tailored to your database schema.
func TestShouldUpdateStats(t *testing.T) {
db, mockDB, err := sqlmock.New()
require.NoError(t, err)
t.Cleanup(db.Close)
mockDB.ExpectQuery(regexp.QuoteMeta(
"SELECT * FROM `token` WHERE (`user_id` = ?);"
)).WithArgs("some-valid-user-id").WillReturnRows(...)
}
Sql mock driver for golang to test database interactions
이렇게 그 동안 자주 사용하고 유용한 패키지를 정리해봤다. 위 내용은 서버 개발에 치중되어 있고, 너무 지엽적인 패키지(e.g. 레벤슈타인 거리 구하는 패키지)는 제외했는데 이 외에도 추천할만한 패키지가 있다면 계속 업데이트 할 예정이다.