posts-go/posts/2022-07-10-bootstrap-go.md

511 lines
14 KiB
Markdown
Raw Normal View History

2022-07-10 10:38:45 +00:00
# Bootstrap Go
2023-08-05 18:56:25 +00:00
It is hard to write bootstrap tool to quickly create Go service. So I write this
guide instead. This is a quick checklist for me every damn time I need to write
a Go service from scratch. Also, this is my personal opinion, so feel free to
comment.
2022-07-10 10:38:45 +00:00
## Structure
```txt
main.go
internal
2023-04-15 08:41:27 +00:00
business
http
handler.go
service.go
models.go
grpc
handler.go
models.go
consumer
handler.go
service.go
models.go
service.go
repository.go
models.go
2022-07-10 10:38:45 +00:00
```
2023-08-05 18:56:25 +00:00
All business codes are inside `internal`. Each business has a different
directory `business`.
2022-07-10 10:38:45 +00:00
Inside each business, there are 2 handlers: `http`, `grpc`:
2022-07-30 17:02:56 +00:00
- `http` is for public APIs (Android, iOS, ... are clients).
2022-07-10 10:38:45 +00:00
- `grpc` is for internal APIs (other services are clients).
2022-07-30 17:02:56 +00:00
- `consumer` is for consuming messages from queue (Kafka, RabbitMQ, ...).
2022-07-10 10:38:45 +00:00
2023-08-05 18:56:25 +00:00
For each handler, there are usually 3 layers: `handler`, `service`,
`repository`:
2022-07-10 10:38:45 +00:00
2023-08-05 18:56:25 +00:00
- `handler` interacts directly with gRPC, REST or consumer using specific codes
(cookies, ...) In case gRPC, there are frameworks outside handle for us so we
can write business/logic codes here too. But remember, gRPC only.
- `service` is where we write business/logic codes, and only business/logic
codes is written here.
- `repository` is where we write codes which interacts with database/cache like
MySQL, Redis, ...
2022-07-30 17:02:56 +00:00
- `models` is where we put all request, response, data models.
2022-07-10 10:38:45 +00:00
2022-07-30 17:10:40 +00:00
Location:
- `handler` must exist inside `grpc`, `http`, `consumer`.
2023-08-05 18:56:25 +00:00
- `service`, `models` can exist directly inside of `business` if both `grpc`,
`http`, `consumer` has same business/logic.
2022-07-30 17:10:40 +00:00
- `repository` should be placed directly inside of `business`.
2022-07-10 10:38:45 +00:00
## Do not repeat!
If we have too many services, some of the logic will be overlapped.
2023-08-05 18:56:25 +00:00
For example, service A and service B both need to make POST call API to service
C. If service A and service B both have libs to call service C to do that API,
we need to move the libs to some common pkg libs. So in the future, service D
which needs to call C will not need to copy libs to handle service C api but
only need to import from common pkg libs.
2022-07-10 10:38:45 +00:00
2023-08-05 18:56:25 +00:00
Another bad practice is adapter service. No need to write a new service if what
we need is just common pkg libs.
2022-07-10 10:38:45 +00:00
2022-07-12 09:55:25 +00:00
## Taste on style guide
2022-08-04 06:24:31 +00:00
### Stop using global var
2022-11-20 08:30:57 +00:00
If I see someone using global var, I swear I will shoot them twice in the face.
2022-08-04 06:24:31 +00:00
Why?
- Can not write unit test.
- Is not thread safe.
2022-07-12 09:55:25 +00:00
### Use functional options, but don't overuse it!
For simple struct with 1 or 2 fields, no need to use functional options.
[Example](https://go.dev/play/p/0XnOLiHuoz3):
```go
func main() {
s := NewS(WithA(1), WithB("b"))
fmt.Printf("%+v\n", s)
}
type S struct {
fieldA int
fieldB string
}
type OptionS func(s *S)
func WithA(a int) OptionS {
return func(s *S) {
s.fieldA = a
}
}
func WithB(b string) OptionS {
return func(s *S) {
s.fieldB = b
}
}
func NewS(opts ...OptionS) *S {
s := &S{}
for _, opt := range opts {
opt(s)
}
return s
}
```
2023-08-05 18:56:25 +00:00
In above example, I construct `s` with `WithA` and `WithB` option. No need to
pass direct field inside `s`.
2022-07-12 09:55:25 +00:00
2022-10-26 14:40:43 +00:00
### Use [errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) as much as possible
2023-08-05 18:56:25 +00:00
If business logic involves calling too many APIs, but they are not depend on
each other. We can fire them parallel :)
2022-10-26 14:40:43 +00:00
2023-08-05 18:56:25 +00:00
Personally, I prefer `errgroup` to `WaitGroup`
(https://pkg.go.dev/sync#WaitGroup). Because I always need deal with error. Be
super careful with `egCtx`, should use this instead of parent `ctx` inside
`eg.Go`.
2022-10-26 14:40:43 +00:00
Example:
2022-11-20 08:30:57 +00:00
```go
2022-10-26 14:40:43 +00:00
eg, egCtx := errgroup.WithContext(ctx)
eg.Go(func() error {
// Do some thing
return nil
})
eg.Go(func() error {
// Do other thing
return nil
})
if err := eg.Wait(); err != nil {
// Handle error
}
```
2022-10-26 14:43:15 +00:00
### Use [semaphore](https://pkg.go.dev/golang.org/x/sync/semaphore) when need to implement WorkerPool
2023-08-05 18:56:25 +00:00
Please don't use external libs for WorkerPool, I don't want to deal with
dependency hell.
2022-10-26 14:43:15 +00:00
2023-03-05 10:23:25 +00:00
### Use [sync.Pool](https://pkg.go.dev/sync#Pool) when need to reuse object, mainly for `bytes.Buffer`
Example:
```go
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func MarshalWithoutEscapeHTML(v any) ([]byte, error) {
b, ok := bufPool.Get().(*bytes.Buffer)
if !ok {
return nil, ErrBufPoolNotBytesBuffer
}
b.Reset()
defer bufPool.Put(b)
encoder := json.NewEncoder(b)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(v); err != nil {
return nil, err
}
result := make([]byte, b.Len())
copy(result, b.Bytes())
return result, nil
}
```
2023-09-24 12:30:39 +00:00
### [Generics](https://go.dev/doc/tutorial/generics) with some tricks
Take value then return pointer, useful with database struct full of pointers:
```go
// Ptr takes in non-pointer and returns a pointer
func Ptr[T any](v T) *T {
return &v
}
```
2023-09-24 16:04:22 +00:00
Return zero value:
```go
func Zero[T any]() T {
var zero T
return zero
}
```
2022-07-10 10:38:45 +00:00
## External libs
2022-07-18 13:53:39 +00:00
### No need `vendor`
2023-08-05 18:56:25 +00:00
Only need if you need something from `vendor`, to generate mock or something
else.
2022-07-18 13:53:39 +00:00
2022-07-30 17:50:41 +00:00
### Use `build.go` to include build tools in go.mod
To easily control version of build tools.
For example `build.go`:
```go
//go:build tools
// +build tools
package main
import (
_ "github.com/golang/protobuf/protoc-gen-go"
)
```
And then in `Makefile`:
```Makefile
build:
go install github.com/golang/protobuf/protoc-gen-go
```
We always get the version of build tools in `go.mod` each time we install it.
Future contributors will not cry anymore.
2022-07-10 10:38:45 +00:00
### Don't use cli libs ([spf13/cobra](https://github.com/spf13/cobra), [urfave/cli](https://github.com/urfave/cli)) just for Go service
2023-08-05 18:56:25 +00:00
What is the point to pass many params (`do-it`, `--abc`, `--xyz`) when what we
only need is start service?
2022-07-10 10:38:45 +00:00
2023-08-05 18:56:25 +00:00
In my case, service starts with only config, and config should be read from file
or environment like [The Twelve Factors](https://12factor.net/) guide.
2022-07-10 10:38:45 +00:00
### Don't use [grpc-ecosystem/grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway)
Just don't.
2023-08-05 18:56:25 +00:00
Use
[protocolbuffers/protobuf-go](https://github.com/protocolbuffers/protobuf-go),
[grpc/grpc-go](https://github.com/grpc/grpc-go) for gRPC.
2022-07-10 10:38:45 +00:00
Write 1 for both gRPC, REST sounds good, but in the end, it is not worth it.
2022-07-10 10:58:57 +00:00
### Don't use [uber/prototool](https://github.com/uber/prototool), use [bufbuild/buf](https://github.com/bufbuild/buf)
2022-07-10 10:38:45 +00:00
2023-08-05 18:56:25 +00:00
prototool is deprecated, and buf can generate, lint, format as good as
prototool.
2022-07-10 10:38:45 +00:00
2022-07-10 10:58:57 +00:00
### Use [gin-gonic/gin](https://github.com/gin-gonic/gin) for REST.
2022-07-10 10:38:45 +00:00
2023-07-17 02:58:46 +00:00
With `c *gin.Context`:
- Don't use `c` when passing context, use `c.Request.Context()` instead.
- Don't use `c.Request.URL.Path`, use `c.FullPath()` instead.
2022-07-10 10:38:45 +00:00
2023-03-05 10:23:25 +00:00
Remember to free resources after parse multipart form:
```go
defer func() {
if err := c.Request.MultipartForm.RemoveAll(); err != nil {
fmt.Println(err)
}
}()
```
2022-07-10 10:38:45 +00:00
### If you want log, just use [uber-go/zap](https://github.com/uber-go/zap)
It is fast!
2023-08-05 18:56:25 +00:00
- Don't overuse `func (*Logger) With`. Because if log line is too long, there is
a possibility that we can lost it.
- Use `MarshalLogObject` when we need to hide some field of object when log
(field is long or has sensitive value)
- Don't use `Panic`. Use `Fatal` for errors when start service to check
dependencies. If you really need panic level, use `DPanic`.
2022-07-21 13:54:35 +00:00
- If doubt, use `zap.Any`.
2022-07-10 10:38:45 +00:00
- Use `contextID` or `traceID` in every log lines for easily debug.
2022-08-09 07:42:42 +00:00
### To read config, use [spf13/viper](https://github.com/spf13/viper)
2023-08-05 18:56:25 +00:00
Only init config in main or cmd layer. Do not use `viper.Get...` in business
layer or inside business layer.
2022-08-09 07:42:42 +00:00
Why?
- Hard to mock and test
- Put all config in single place for easily tracking
2023-08-05 18:56:25 +00:00
Also, be careful if config value is empty. You should decide to continue or stop
the service if there is empty config.
2022-12-08 09:48:43 +00:00
2022-07-10 10:38:45 +00:00
### Don't overuse ORM libs, no need to handle another layer above SQL.
2023-08-05 18:56:25 +00:00
Each ORM libs has each different syntax. To learn and use those libs correctly
is time consuming. So just stick to plain SQL. It is easier to debug when
something is wrong.
2022-07-10 10:38:45 +00:00
2023-08-05 18:56:25 +00:00
Also please use
[prepared statement](https://go.dev/doc/database/prepared-statements) as much as
possible. Idealy, we should init all prepared statement when we init database
connection to cached it, not create it every time we need it.
2023-05-02 17:38:15 +00:00
2023-08-05 18:56:25 +00:00
But `database/sql` has its own limit. For example, it is hard to get primary key
after insert/update. So may be you want to use ORM for those cases. I hear that
2023-09-19 10:41:00 +00:00
[go-gorm/gorm](https://github.com/go-gorm/gorm) is good.
2022-07-10 10:38:45 +00:00
2023-03-05 09:10:27 +00:00
### Connect Redis with [redis/go-redis](https://github.com/redis/go-redis)
2023-08-05 18:56:25 +00:00
Be careful when use [HGETALL](https://redis.io/commands/hgetall/). If key not
found, empty data will be returned not nil error. See
[redis/go-redis/issues/1668](https://github.com/redis/go-redis/issues/1668)
2023-07-19 03:34:07 +00:00
2023-03-05 09:10:27 +00:00
Use [Pipelines](https://redis.uptrace.dev/guide/go-redis-pipelines.html) for:
- HSET and EXPIRE in 1 command.
- Multiple GET in 1 command.
2023-08-05 18:56:25 +00:00
Prefer to use `Pipelined` instead of `Pipeline`. Inside `Pipelined`, please
return `redis.Cmder` for each command.
2023-03-05 09:10:27 +00:00
Example:
```go
func (c *client) HSetWithExpire(ctx context.Context, key string, values []any, expired time.Duration) error {
cmds := make([]redis.Cmder, 2)
if _, err := c.Pipelined(ctx, func(pipe redis.Pipeliner) error {
cmds[0] = pipe.HSet(ctx, key, values...)
if expired > 0 {
cmds[1] = pipe.Expire(ctx, key, expired)
}
return nil
}); err != nil {
return err
}
for _, cmd := range cmds {
if cmd == nil {
continue
}
if err := cmd.Err(); err != nil {
return err
}
}
return nil
}
```
2023-06-10 14:29:59 +00:00
Remember to config:
- `ReadTimeout`, `WriteTimeout`
2023-03-05 09:44:02 +00:00
### Connect MySQL with [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)
Remember to config:
- `SetConnMaxLifetime`
- `SetMaxOpenConns`
- `SetMaxIdleConns`
- `ParseTime` to true.
- `Loc` to `time.UTC`.
- `CheckConnLiveness` to true.
2023-06-10 14:29:59 +00:00
- `ReadTimeout`, `WriteTimeout`
2023-03-05 09:44:02 +00:00
2023-05-02 17:38:15 +00:00
### Connect SQLite with [modernc.org/sqlite](https://gitlab.com/cznic/sqlite)
Remember to config:
- Write-Ahead Logging: `PRAGMA journal_mode=WAL`
- Disable connections pool with `SetMaxOpenConns` sets to 1
2023-08-05 18:56:25 +00:00
Don't use [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3), it's required
`CGO_ENABLED`.
2023-05-02 17:38:15 +00:00
2023-09-19 10:41:00 +00:00
### Connect Kafka with [IBM/sarama](https://github.com/IBM/sarama)
Use `sarama.V1_0_0_0`, because IBM decide to upgrade default version.
2023-03-21 08:15:40 +00:00
2023-08-05 18:56:25 +00:00
Don't use
[confluentinc/confluent-kafka-go](https://github.com/confluentinc/confluent-kafka-go),
it's required `CGO_ENABLED`.
2023-03-21 08:15:40 +00:00
2022-07-10 10:38:45 +00:00
### If you want test, just use [stretchr/testify](https://github.com/stretchr/testify).
2023-08-05 18:56:25 +00:00
It is easy to write a suite test, thanks to testify. Also, for mocking, there
are many options out there. Pick 1 then sleep peacefully.
2022-07-10 10:58:57 +00:00
2023-06-28 11:58:50 +00:00
### If need to mock, choose [matryer/moq](https://github.com/matryer/moq) or [uber/mock](https://github.com/uber/mock)
2022-09-27 17:26:40 +00:00
2023-08-05 18:56:25 +00:00
The first is easy to use but not powerful as the later. If you want to make sure
mock func is called with correct times, use the later.
2022-09-27 17:26:40 +00:00
2022-10-09 10:35:16 +00:00
Example with `matryer/moq`:
2022-11-20 08:30:57 +00:00
```go
2022-10-09 10:35:16 +00:00
// Only gen mock if source code file is newer than mock file
// https://jonwillia.ms/2019/12/22/conditional-gomock-mockgen
//go:generate sh -c "test service_mock_generated.go -nt $GOFILE && exit 0; moq -rm -out service_mock_generated.go . Service"
```
2022-10-26 14:33:20 +00:00
### Be careful with [spf13/cast](https://github.com/spf13/cast)
Don't cast proto enum:
2022-11-20 08:30:57 +00:00
```go
2022-11-20 09:35:49 +00:00
// Bad
2022-10-26 14:33:20 +00:00
a := cast.ToInt32(servicev1.ReasonCode_ABC)
2022-11-20 09:35:49 +00:00
// Good
2022-10-26 14:33:20 +00:00
a := int32(servicev1.ReasonCode_ABC)
```
2022-11-20 08:30:57 +00:00
### Use [stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) if you want your type enum can be print as string
```go
type Drink int
const (
Beer Drink = iota
Water
OrangeJuice
)
```
```sh
go install golang.org/x/tools/cmd/stringer@latest
# Run inside directory which contains Drink
stringer -type=Drink
```
2022-11-20 09:10:53 +00:00
### Don't waste your time rewrite rate limiter if your use case is simple, use [rate](https://pkg.go.dev/golang.org/x/time/rate) or [go-redis/redis_rate](https://github.com/go-redis/redis_rate)
2023-03-05 10:23:25 +00:00
**rate** if you want rate limiter locally in your single instance of service.
2023-08-05 18:56:25 +00:00
**redis_rate** if you want rate limiter distributed across all your instances of
service.
2022-11-20 09:10:53 +00:00
2022-07-10 10:58:57 +00:00
### Replace `go fmt`, `goimports` with [mvdan/gofumpt](https://github.com/mvdan/gofumpt).
`gofumpt` provides more rules when format Go codes.
### Use [golangci/golangci-lint](https://github.com/golangci/golangci-lint).
2023-08-05 18:56:25 +00:00
No need to say more. Lint or get the f out!
2022-07-12 09:55:25 +00:00
2023-08-05 18:56:25 +00:00
If you get `fieldalignment` error, use
[fieldalignment](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment)
to fix them.
2022-07-30 17:02:56 +00:00
```sh
# Install
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
2022-07-30 17:02:56 +00:00
# Fix
fieldalignment -fix ./internal/business/*.go
```
2023-03-18 09:10:48 +00:00
## Snippet/script
Change import:
```sh
gofmt -w -r '"github.com/Sirupsen/logrus" -> "github.com/sirupsen/logrus"' *.go
```
2023-06-04 13:57:18 +00:00
Cleanup if storage is full:
```sh
go clean -cache -testcache -modcache -fuzzcache -x
```
2022-07-12 09:55:25 +00:00
## Thanks
- [Functional options for friendly APIs](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis)
2023-08-17 10:06:57 +00:00
- [Designing Go Libraries: The Talk: The Article](https://abhinavg.net/2022/12/06/designing-go-libraries/)
- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
2022-11-20 08:39:24 +00:00
- [Google Go Style](https://google.github.io/styleguide/go/index)
2023-03-05 09:44:02 +00:00
- [Three bugs in the Go MySQL Driver](https://github.blog/2020-05-20-three-bugs-in-the-go-mysql-driver/)
2023-03-05 10:23:25 +00:00
- [Fixing Memory Exhaustion Bugs in My Golang Web App](https://mtlynch.io/notes/picoshare-perf/)
- [Prevent Logging Secrets in Go by Using Custom Types](https://www.commonfate.io/blog/prevent-logging-secrets-in-go-by-using-custom-types)
- [Speed Up GoMock with Conditional Generation](https://jonwillia.ms/2019/12/22/conditional-gomock-mockgen)
2023-05-02 17:38:15 +00:00
- [Making SQLite faster in Go](https://turriate.com/articles/making-sqlite-faster-in-go)
2023-09-19 10:41:00 +00:00
- [Advanced Go Concurrency](https://encore.dev/blog/advanced-go-concurrency)
2023-09-24 12:30:39 +00:00
- [Go generic: non-ptr to ptr](https://danielms.site/zet/2023/go-generic-non-ptr-to-ptr/)
2023-09-24 16:04:22 +00:00
- [Crimes with Go Generics](https://xeiaso.net/blog/gonads-2022-04-24)