7.1 KiB
Bootstrap Go
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.
Structure
main.go
internal
| 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
All business codes are inside internal
.
Each business has a different directory business
.
Inside each business, there are 2 handlers: http
, grpc
:
http
is for public APIs (Android, iOS, ... are clients).grpc
is for internal APIs (other services are clients).consumer
is for consuming messages from queue (Kafka, RabbitMQ, ...).
For each handler, there are usually 3 layers: handler
, service
, repository
:
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, ...models
is where we put all request, response, data models.
Location:
handler
must exist insidegrpc
,http
,consumer
.service
,models
can exist directly inside ofbusiness
if bothgrpc
,http
,consumer
has same business/logic.repository
should be placed directly inside ofbusiness
.
Do not repeat!
If we have too many services, some of the logic will be overlapped.
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.
Another bad practice is adapter service. No need to write a new service if what we need is just common pkg libs.
Taste on style guide
Stop using global var
If I see someone using global var, I swear I shoot twice in the face.
Why?
- Can not write unit test.
- Is not thread safe.
Use functional options, but don't overuse it!
For simple struct with 1 or 2 fields, no need to use functional options.
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
}
In above example, I construct s
with WithA
and WithB
option.
No need to pass direct field inside s
.
External libs
No need vendor
Only need if you need something from vendor
, to generate mock or something else.
Use build.go
to include build tools in go.mod
To easily control version of build tools.
For example build.go
:
//go:build tools
// +build tools
package main
import (
_ "github.com/golang/protobuf/protoc-gen-go"
)
And then in 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.
Don't use cli libs (spf13/cobra, urfave/cli) just for Go service
What is the point to pass many params (do-it
, --abc
, --xyz
) when what we only need is start service?
In my case, service starts with only config, and config should be read from file or environment like The Twelve Factors guide.
Don't use grpc-ecosystem/grpc-gateway
Just don't.
Use protocolbuffers/protobuf-go, grpc/grpc-go for gRPC.
Write 1 for both gRPC, REST sounds good, but in the end, it is not worth it.
Don't use uber/prototool, use bufbuild/buf
prototool is deprecated, and buf can generate, lint, format as good as prototool.
Use gin-gonic/gin for REST.
Don't use gin.Context
when pass context from handler layer to service layer, use gin.Context.Request.Context()
instead.
If you want log, just use uber-go/zap
It is fast!
-
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
. UseFatal
for errors when start service to check dependencies. If you really need panic level, useDPanic
. -
If doubt, use
zap.Any
. -
Use
contextID
ortraceID
in every log lines for easily debug.
To read config, use spf13/viper
Only init config in main or cmd layer.
Do not use viper.Get...
in business layer or inside business layer.
Why?
- Hard to mock and test
- Put all config in single place for easily tracking
Don't overuse ORM libs, no need to handle another layer above SQL.
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.
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.
If you want test, just use stretchr/testify.
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.
If need to mock, choose matryer/moq or golang/mock
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.
Replace go fmt
, goimports
with mvdan/gofumpt.
gofumpt
provides more rules when format Go codes.
Use golangci/golangci-lint.
No need to say more. Lint or get the f out!
If you get fieldalignment
error, use fieldalignment to fix them.
# Install
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
# Fix
fieldalignment -fix ./internal/business/*.go