From c4bf46c7c57cc6ade699dc429b31651186994ba2 Mon Sep 17 00:00:00 2001 From: Hau Nguyen Date: Sun, 31 Jul 2022 00:02:56 +0700 Subject: [PATCH] feat: update bootstrap go --- docs/2022-07-10-bootstrap-go.html | 22 ++++++++--------- posts/2022-07-10-bootstrap-go.md | 41 ++++++++++++++++++------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/docs/2022-07-10-bootstrap-go.html b/docs/2022-07-10-bootstrap-go.html index 57feef1..9a38ab8 100644 --- a/docs/2022-07-10-bootstrap-go.html +++ b/docs/2022-07-10-bootstrap-go.html @@ -1,26 +1,21 @@ Index

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_1
+| business
 | | http
 | | | handler.go
 | | | service.go
-| | | repository.go
 | | | models.go
 | | grpc
 | | | handler.go
+| | | models.go
+| | consumer
+| | | handler.go
 | | | service.go
-| | | repository.go
 | | | models.go
 | | service.go
 | | repository.go
 | | models.go
-| business_2
-| | grpc
-| | | handler.go
-| | | service.go
-| | | repository.go
-| | | models.go
-

All business codes are inside internal.
Each business has a different directory (business_1, business_2).

Inside each business, there are 2 handlers: http, grpc:

Inside each handler, there are usually 3 layers: handler, service, repository:

handler must exist inside grpc, http.
But service, repository, models can exist directly inside business if both grpc, http has same business/logic.

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

Use functional options, but don't overuse it!

For simple struct with 1 or 2 fields, no need to use functional options.

Example:

func main() {
+

All business codes are inside internal.
Each business has a different directory business.

Inside each business, there are 2 handlers: http, grpc:

For each handler, there are usually 3 layers: handler, service, repository:

handler must exist inside grpc, http.
But service, models can exist directly inside of business if both grpc, http has same business/logic.

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

Use functional options, but don't overuse it!

For simple struct with 1 or 2 fields, no need to use functional options.

Example:

func main() {
 	s := NewS(WithA(1), WithB("b"))
 	fmt.Printf("%+v\n", s)
 }
@@ -51,4 +46,9 @@ func NewS(opts ...OptionS) *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.

Don't use cli libs (spf13/cobra, urfave/cli) just for Go service

What is the point to pass many params (--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 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.

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!

Thanks

Feel free to ask me via email \ No newline at end of file +

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.

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 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.

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@latest
+
+# Fix
+fieldalignment -fix ./internal/business/*.go
+

Thanks

Feel free to ask me via email \ No newline at end of file diff --git a/posts/2022-07-10-bootstrap-go.md b/posts/2022-07-10-bootstrap-go.md index 27914c2..121d33b 100644 --- a/posts/2022-07-10-bootstrap-go.md +++ b/posts/2022-07-10-bootstrap-go.md @@ -10,44 +10,41 @@ Also, this is my personal opinion, so feel free to comment. ```txt main.go internal -| business_1 +| business | | http | | | handler.go | | | service.go -| | | repository.go | | | models.go | | grpc | | | handler.go +| | | models.go +| | consumer +| | | handler.go | | | service.go -| | | repository.go | | | models.go | | service.go | | repository.go | | models.go -| business_2 -| | grpc -| | | handler.go -| | | service.go -| | | repository.go -| | | models.go ``` All business codes are inside `internal`. -Each business has a different directory (`business_1`, `business_2`). +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). +- `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, ...). -Inside each handler, there are usually 3 layers: `handler`, `service`, `repository`: +For each handler, there are usually 3 layers: `handler`, `service`, `repository`: -- `handler` interacts directly with gRPC or REST using specific codes (cookies,...) +- `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, ... +- `repository` is where we write codes which interacts with database/cache like MySQL, Redis, ... And we should place it directly inside of `business`. +- `models` is where we put all request, response, data models. `handler` must exist inside `grpc`, `http`. -But `service`, `repository`, `models` can exist directly inside `business` if both `grpc`, `http` has same business/logic. +But `service`, `models` can exist directly inside of `business` if both `grpc`, `http` has same business/logic. ## Do not repeat! @@ -113,7 +110,7 @@ Only need if you need something from `vendor`, to generate mock or something els ### Don't use cli libs ([spf13/cobra](https://github.com/spf13/cobra), [urfave/cli](https://github.com/urfave/cli)) just for Go service -What is the point to pass many params (`--abc`, `--xyz`) when what we only need is start 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](https://12factor.net/) guide. @@ -139,7 +136,7 @@ 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 has long or sensitive value) +- 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`. @@ -173,6 +170,16 @@ Pick 1 then sleep peacefully. No need to say more. Lint or get the f out! +If you get `fieldalignment` error, use [fieldalignment](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment) to fix them. + +```sh +# Install +go install golang.org/x/tools/go/analysis/passes/fieldalignment@latest + +# Fix +fieldalignment -fix ./internal/business/*.go +``` + ## Thanks - [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)