Golang微服务最佳实践
Jul 24, 2019
本文通过一个完整的项目的示例,从Golang项目的结构、分层思想、依赖注入、错误处理、单元测试、框架选择等方面介绍Go语言项目的开发经验.
目的
- 提供一个完整的go语言项目编程示例
- 通过示例项目介绍go语言中的编程思想
- 介绍项目实例遵守的规范
示例项目
Github源码go-project-sample
包结构
关于golang项目的包结构,Dave Chaney博客《Five suggestions for setting up a Go project》里讨论了package和command的包设计建议,还有一个社区普遍认可的包结构规范project-layout。在这两个两篇文章的知道下,结合常见的互联网微服务项目,我又细化了如下的项目结构。
.
├── Makefile
├── README.md
├── api
│ └── proto
│ ├── products.pb.go
│ └── products.proto
├── cmd
│ └── sample
│ ├── main.go
│ ├── wire.go
│ └── wire_gen.go
├── configs
│ └── sample.yml
├── dist
│ ├── sample-darwin-amd64
│ ├── sample-linux-amd64
│ └── test
│ └── cover.out
├── docker
│ └── sample
│ ├── Dockerfile
│ └── docker-compose.yml
├── go.mod
├── go.sum
├── internal
│ ├── app
│ │ ├── sample
│ │ │ ├── app.go
│ │ │ ├── controllers
│ │ │ │ ├── controllers.go
│ │ │ │ ├── products.go
│ │ │ │ ├── products_test.go
│ │ │ │ ├── wire.go
│ │ │ │ └── wire_gen.go
│ │ │ └── grpcservers
│ │ │ ├── products.go
│ │ │ ├── products_test.go
│ │ │ ├── servers.go
│ │ │ ├── wire.go
│ │ │ └── wire_gen.go
│ │ └── tool
│ │ └── clients
│ │ ├── clients.go
│ │ ├── products.go
│ │ ├── products_test.go.bak
│ │ └── wire.go
│ └── pkg
│ ├── config
│ │ ├── config.go
│ │ └── wire.go
│ ├── database
│ │ └── database.go
│ ├── log
│ │ └── log.go
│ ├── models
│ │ └── product.go
│ ├── repositorys
│ │ ├── cover.out
│ │ ├── mock_ProductsStorage.go
│ │ ├── products.go
│ │ ├── products_test.go
│ │ ├── repositorys.go
│ │ ├── wire.go
│ │ └── wire_gen.go
│ ├── services
│ │ ├── mock_ProductsService.go
│ │ ├── products.go
│ │ ├── products_test.go
│ │ ├── services.go
│ │ ├── wire.go
│ │ └── wire_gen.go
│ └── transport
│ ├── grpc
│ │ └── grpc.go
│ └── http
│ └── http.go
└── scripts
├── sample.sql
└── wait-for-it.sh
\cmd
“该项目的main方法。 每个应用程序的目录名称应与您要拥有的可执行文件的名称相匹配(例如,/cmd/myapp)。 不要在应用程序目录中放入大量代码。 如果您认为代码可以导入并在其他项目中使用,那么它应该存在于/ pkg目录中。 如果代码不可重用或者您不希望其他人重用它,请将该代码放在/ internal目录中。 你会惊讶于别人会做什么,所以要明确你的意图! 通常有一个小的main函数可以从/ internal和/ pkg目录中导入和调用代码,而不是其他任何东西。”
\internal\pkg
“私有应用程序和库代码。 这是您不希望其他人在其应用程序或库中导入的代码。 将您的实际应用程序代码放在/internal/app目录(例如/internal/app/myapp)和/internal/ pkg目录中这些应用程序共享的代码(例如/internal/pkg/myprivlib)。”
内部的包采用平铺的方式。
\internal\pkg\config
加载配置文件,或者从配置中心获取配置和监听配置变动。
\internal\pkg\database
数据库连接初始化和ORM框架初始化配置。
\internal\pkg\models
结构体定义。
\internal\pkg\repositrys
存储层逻辑代码。
\internal\pkg\services
领域逻辑层代码。
\internal\pkg\transport
传输层/控制层逻辑代码
\internal\app\proxy
应用内部代码
\internal\app\controllers
http handlers
\internal\app\grpcserver
grpc server 实现
/api
OpenAPI/Swagger规范,JSON模式文件,协议定义文件等。
/scripts
sql、部署等脚本
/docker
Dockerfile、docker-compose
分层
MVC、领域模型、ORM 这些都是通过把特定职责的代码拆分到不同的层次对象里,在Java里这些分层概念在各种框架里都有体现(如SSH,SSM等常用框架组合),并且早已形成了默认的规约,是否还适用go语言吗?答案是肯定的。Martin Fowler在《企业应用架构模式》就阐述过分层带来的各种好处。
- 便代码复用,提高代码可维护性.如service的代码可被http协议和grpc协议复用,如果增加thrift协议的接口也很方便。
- 层次清晰,代码可读性更高。
- 方便单元测试,单元测试往往因为依赖持久的存储而无法进行,如果持久化代码抽取到单独的对象里,这就变的很简单了.
依赖注入
Java 程序员都很熟悉依赖注入和控制翻转这种思想,Spring正式基于依赖注入的思想开发。依赖注入的好处是解耦,对象的组装交给容器来控制(选择需要的实现类、是否单例和初始化).基于依赖注入可以很方便的实现单元测试和提高代码可维护性。
关于Golang依赖注入的讨论《Dependency Injection in Go》,Golang依赖注入的Package有 Uber的dig,fx,facebook 的 inject,google的wire。dig、fx和inject都是基于反射实现,wire是通过代码生成实现,代码生成的方式是显式的。
本示例通过wire来完成依赖注入. 编写wire.go,wire会根据wire.go生成代码。
// +build wireinject
package main
import (
"github.com/google/wire"
"github.com/zlgwzy/go-project-sample/cmd/app"
"github.com/zlgwzy/go-project-sample/internal/pkg/config"
"github.com/zlgwzy/go-project-sample/internal/pkg/database"
"github.com/zlgwzy/go-project-sample/internal/pkg/log"
"github.com/zlgwzy/go-project-sample/internal/pkg/services"
"github.com/zlgwzy/go-project-sample/internal/pkg/repositorys"
"github.com/zlgwzy/go-project-sample/internal/pkg/transport/http"
"github.com/zlgwzy/go-project-sample/internal/pkg/transport/grpc"
)
var providerSet = wire.NewSet(
log.ProviderSet,
config.ProviderSet,
database.ProviderSet,
services.ProviderSet,
repositorys.ProviderSet,
http.ProviderSet,
grpc.ProviderSet,
app.ProviderSet,
)
func CreateApp(cf string) (*app.App, error) {
panic(wire.Build(providerSet))
}
生成代码
go get github.com/google/wire/cmd/wire
wire ./...
生成后的代码在wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
import (
"github.com/google/wire"
"github.com/zlgwzy/go-project-sample/cmd/app"
"github.com/zlgwzy/go-project-sample/internal/pkg/config"
"github.com/zlgwzy/go-project-sample/internal/pkg/database"
"github.com/zlgwzy/go-project-sample/internal/pkg/log"
"github.com/zlgwzy/go-project-sample/internal/pkg/services"
"github.com/zlgwzy/go-project-sample/internal/pkg/repositorys"
"github.com/zlgwzy/go-project-sample/internal/pkg/transport/grpc"
"github.com/zlgwzy/go-project-sample/internal/app/proxy/grpcservers"
"github.com/zlgwzy/go-project-sample/internal/pkg/transport/http"
"github.com/zlgwzy/go-project-sample/internal/app/proxy/controllers"
)
// Injectors from wire.go:
func CreateApp(cf string) (*app.App, error) {
viper, err := config.New(cf)
if err != nil {
return nil, err
}
options, err := log.NewOptions(viper)
if err != nil {
return nil, err
}
logger, err := log.New(options)
if err != nil {
return nil, err
}
httpOptions, err := http.NewOptions(viper)
if err != nil {
return nil, err
}
databaseOptions, err := database.NewOptions(viper, logger)
if err != nil {
return nil, err
}
db, err := database.New(databaseOptions)
if err != nil {
return nil, err
}
productsRepository := repositorys.NewMysqlProductsRepository(logger, db)
productsService := services.NewProductService(logger, productsRepository)
productsController := controllers.NewProductsController(logger, productsService)
initControllers := controllers.CreateInitControllersFn(productsController)
engine := http.NewRouter(httpOptions, initControllers)
server, err := http.New(httpOptions, logger, engine)
if err != nil {
return nil, err
}
grpcOptions, err := grpc.NewOptions(viper)
if err != nil {
return nil, err
}
productsServer, err := grpcservers.NewProductsServer(logger, productsService)
if err != nil {
return nil, err
}
initServers := grpcservers.CreateInitServersFn(productsServer)
grpcServer, err := grpc.New(grpcOptions, logger, initServers)
if err != nil {
return nil, err
}
appApp, err := app.New(logger, server, grpcServer)
if err != nil {
return nil, err
}
return appApp, nil
}
// wire.go:
var providerSet = wire.NewSet(log.ProviderSet, config.ProviderSet, database.ProviderSet, services.ProviderSet, repositorys.ProviderSet, http.ProviderSet, grpc.ProviderSet, app.ProviderSet)
面向接口编程
多态和单元测试必须,比较好理解不再解释。
显式编程
Golang的开发推崇这样一种显式编程的思想,显式的初始化、方法调用和错误处理.
- 尽可能不要使用包级别的全局变量.
- 尽量不要使用init函数,初始化操作可以在main函数中调用,这样方便阅读代码和控制初始化顺序。
- 函数都要返回错误,用if err != nil 显式的处理错误.
- 依赖的参数让调用者去控制(控制翻转的思想),可以看下节依赖注入。
几个大佬都讨论过这个问题,博士Peter的《A theory of modern Go》认为魔法代码的核心是”no package level vars; no func init“.单这也不是绝对。 Dave Cheny在《go-without-package-scoped-variables》做了更详细的说明.
打印日志
使用比较多的两个日志库,logrush和zap,个人更喜欢zap。
初始化logger,通过viper加载日志相关配置,lumberjack负责日志切割。
// Options is log configration struct
type Options struct {
Filename string
MaxSize int
MaxBackups int
MaxAge int
Level string
Stdout bool
}
func NewOptions(v *viper.Viper) (*Options, error) {
var (
err error
o = new(Options)
)
if err = v.UnmarshalKey("log", o); err != nil {
return nil, err
}
return o, err
}
// New for init zap log library
func New(o *Options) (*zap.Logger, error) {
var (
err error
level = zap.NewAtomicLevel()
logger *zap.Logger
)
err = level.UnmarshalText([]byte(o.Level))
if err != nil {
return nil, err
}
fw := zapcore.AddSync(&lumberjack.Logger{
Filename: o.Filename,
MaxSize: o.MaxSize, // megabytes
MaxBackups: o.MaxBackups,
MaxAge: o.MaxAge, // days
})
cw := zapcore.Lock(os.Stdout)
// file core 采用jsonEncoder
cores := make([]zapcore.Core, 0, 2)
je := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
cores = append(cores, zapcore.NewCore(je, fw, level))
// stdout core 采用 ConsoleEncoder
if o.Stdout {
ce := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
cores = append(cores, zapcore.NewCore(ce, cw, level))
}
core := zapcore.NewTee(cores...)
logger = zap.New(core)
zap.ReplaceGlobals(logger)
return logger, err
}
logger应该作为私有变量,这样可以统一添加对象的标示。
type Object struct {
logger *zap.Logger
}
// 统一添加标示
func NewObject(logger *zap.Logger){
return &Object{
logger: logger.With(zap.String("type","Object"))
}
}
错误处理
错误处理还是看Dave Cheny的博客《Stack traces and the errors package》,《Don’t just check errors, handle them gracefully》。
- 使用类型判断错误。
- 包装错误,记录错误的上下文。
- 使用 pakcage errors
- 只处理一次错误,处理错误意味着检查错误值并做出决定。
错误输出示例
// pc.logger.Error("get product by id error", zap.Error(err))
{
"level":"error",
"ts":1564056905.4602501,
"msg":"get product by id error",
"error":"product service get product error: get product error[id=2]: record not found",
"errorVerbose":"record not found get product error[id=2]
github.com/zlgwzy/go-project-sample/internal/pkg/repositorys.(*MysqlProductsRepository).Get
/Users/xxx/code/go/go-project-sample/internal/pkg/repositorys/products.go:29
github.com/zlgwzy/go-project-sample/internal/pkg/services.(*DefaultProductsService).Get
/Users/xxx/code/go/go-project-sample/internal/pkg/services/products.go:27
github.com/zlgwzy/go-project-sample/internal/app/proxy/controllers.(*ProductsController).Get
/Users/xxx/code/go/go-project-sample/internal/app/proxy/controllers/products.go:30
github.com/gin-gonic/gin.(*Context).Next
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124
github.com/gin-gonic/gin.RecoveryWithWriter.func1
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/recovery.go:83
github.com/gin-gonic/gin.(*Context).Next
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/gin.go:389
github.com/gin-gonic/gin.(*Engine).ServeHTTP
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/gin.go:351
net/http.serverHandler.ServeHTTP
/usr/local/Cellar/go/1.12.6/libexec/src/net/http/server.go:2774
net/http.(*conn).serve
/usr/local/Cellar/go/1.12.6/libexec/src/net/http/server.go:1878
runtime.goexit
/usr/local/Cellar/go/1.12.6/libexec/src/runtime/asm_amd64.s:1337
product service get product error
github.com/zlgwzy/go-project-sample/internal/pkg/services.(*DefaultProductsService).Get
/Users/xxx/code/go/go-project-sample/internal/pkg/services/products.go:28
github.com/zlgwzy/go-project-sample/internal/app/proxy/controllers.(*ProductsController).Get
/Users/xxx/code/go/go-project-sample/internal/app/proxy/controllers/products.go:30
github.com/gin-gonic/gin.(*Context).Next
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124
github.com/gin-gonic/gin.RecoveryWithWriter.func1
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/recovery.go:83
github.com/gin-gonic/gin.(*Context).Next
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/context.go:124
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/gin.go:389
github.com/gin-gonic/gin.(*Engine).ServeHTTP
/Users/xxx/go/pkg/mod/github.com/gin-gonic/gin@v1.4.0/gin.go:351
net/http.serverHandler.ServeHTTP
/usr/local/Cellar/go/1.12.6/libexec/src/net/http/server.go:2774
net/http.(*conn).serve
/usr/local/Cellar/go/1.12.6/libexec/src/net/http/server.go:1878
runtime.goexit
/usr/local/Cellar/go/1.12.6/libexec/src/runtime/asm_amd64.s:1337"
}
单元测试
存储层测试
添加repositorys/wire.go 创建要测试的对象,会根据ProviderSet注入合适的依赖。
// +build wireinject
package repositorys
import (
"github.com/google/wire"
"github.com/zlgwzy/go-project-sample/internal/pkg/config"
"github.com/zlgwzy/go-project-sample/internal/pkg/database"
"github.com/zlgwzy/go-project-sample/internal/pkg/log"
)
var testProviderSet = wire.NewSet(
log.ProviderSet,
config.ProviderSet,
database.ProviderSet,
ProviderSet,
)
func CreateProductRepository(f string) (ProductsRepository, error) {
panic(wire.Build(testProviderSet))
}
添加repositorys/products_test.go,这里采用表格驱动的方法进行测试,存储层测试会依赖数据库。
package repositorys
import (
"flag"
"github.com/stretchr/testify/assert"
"testing"
)
var configFile = flag.String("f", "app.yml", "set config file which viper will loading.")
func TestProductsRepository_Get(t *testing.T) {
flag.Parse()
sto, err := CreateProductRepository(*configFile)
if err != nil {
t.Fatalf("create product Repository error,%+v", err)
}
tests := []struct {
name string
id uint64
expected bool
}{
{"1+1", 1, true},
{"2+3", 2, false},
{"4+5", 3, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := sto.Get(test.id)
if test.expected {
assert.NoError(t, err )
}else {
assert.Error(t, err)
}
})
}
}
运行测试
cd internal/pkg/repositorys
go test . -v -f ../../../configs/proxy.yml
=== RUN TestProductsRepository_Get
2019/07/26 14:29:28 use config file:../../../configs/proxy.yml
2019-07-26T14:29:28.301+0800 INFO load log options success {"url": "root:xxx@tcp(127.0.0.1:3306)/shop?charset=utf8&parseTime=True&loc=Local"}
=== RUN TestProductsRepository_Get/1+1
=== RUN TestProductsRepository_Get/2+3
=== RUN TestProductsRepository_Get/4+5
--- PASS: TestProductsRepository_Get (0.04s)
--- PASS: TestProductsRepository_Get/1+1 (0.00s)
--- PASS: TestProductsRepository_Get/2+3 (0.00s)
--- PASS: TestProductsRepository_Get/4+5 (0.00s)
PASS
ok github.com/zlgwzy/go-project-sample/internal/pkg/repositorys 0.049s
逻辑层测试
通过mockery自动生成mock对象.
mockery --all --inpkg
添加services/wire.go
// +build wireinject
package services
import (
"github.com/google/wire"
"github.com/zlgwzy/go-project-sample/internal/pkg/config"
"github.com/zlgwzy/go-project-sample/internal/pkg/database"
"github.com/zlgwzy/go-project-sample/internal/pkg/log"
"github.com/zlgwzy/go-project-sample/internal/pkg/repositorys"
)
var testProviderSet = wire.NewSet(
log.ProviderSet,
config.ProviderSet,
database.ProviderSet,
ProviderSet,
)
func CreateProductsService(cf string, sto repositorys.ProductsRepository) (ProductsService, error) {
panic(wire.Build(testProviderSet))
}
编写单元测试services/products_test.go
package services
import (
"flag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/zlgwzy/go-project-sample/internal/pkg/models"
"github.com/zlgwzy/go-project-sample/internal/pkg/repositorys"
"testing"
)
var configFile = flag.String("f", "proxy.yml", "set config file which viper will loading.")
func TestProductsRepository_Get(t *testing.T) {
flag.Parse()
sto := new(repositorys.MockProductsRepository)
sto.On("Get", mock.AnythingOfType("uint64")).Return(func(ID uint64) (p *models.Product) {
return &models.Product{
ID: ID,
}
}, func(ID uint64) error {
return nil
})
svc, err := CreateProductsService(*configFile, sto)
if err != nil {
t.Fatalf("create product serviceerror,%+v", err)
}
// 表格驱动测试
tests := []struct {
name string
id uint64
expected uint64
}{
{"1+1", 1, 1},
{"2+3", 2, 2},
{"4+5", 3, 3},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
p, err := svc.Get(test.id)
if err != nil {
t.Fatalf("product service get proudct error,%+v", err)
}
assert.Equal(t, test.expected, p.ID)
})
}
}
存储层使用生成的MockProductsRepository,可以直接在用例中定义Mock方法的返回值。
控制层测试
添加controllers/products_test.go,利用httptest进行测试
package controllers
import (
"encoding/json"
"flag"
"fmt"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"io/ioutil"
"net/http/httptest"
"github.com/zlgwzy/go-project-sample/internal/pkg/models"
"github.com/zlgwzy/go-project-sample/internal/pkg/repositorys"
"testing"
)
var r *gin.Engine
var configFile = flag.String("f", "proxy.yml", "set config file which viper will loading.")
func setup() {
r = gin.New()
}
func TestProductsController_Get(t *testing.T) {
flag.Parse()
setup()
sto := new(repositorys.MockProductsRepository)
sto.On("Get", mock.AnythingOfType("uint64")).Return(func(ID uint64) (p *models.Product) {
return &models.Product{
ID: ID,
}
}, func(ID uint64) error {
return nil
})
c, err := CreateProductsController(*configFile, sto)
if err != nil {
t.Fatalf("create product serviceerror,%+v", err)
}
r.GET("/products/:id", c.Get)
tests := []struct {
name string
id uint64
expected uint64
}{
{"1", 1, 1},
{"2", 2, 2},
{"3", 3, 3},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
uri := fmt.Sprintf("/products/%d", test.id)
// 构造get请求
req := httptest.NewRequest("GET", uri, nil)
// 初始化响应
w := httptest.NewRecorder()
// 调用相应的controller接口
r.ServeHTTP(w, req)
// 提取响应
rs := w.Result()
defer func() {
_ = rs.Body.Close()
}()
// 读取响应body
body, _ := ioutil.ReadAll(rs.Body)
p := new(models.Product)
err := json.Unmarshal(body, p)
if err != nil {
t.Errorf("unmarshal response body error:%v", err)
}
assert.Equal(t, test.expected, p.ID)
})
}
}
grpc测试
添加grpcservers/products_test.go
package grpcservers
import (
"context"
"flag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/zlgwzy/go-project-sample/internal/pkg/models"
"github.com/zlgwzy/go-project-sample/internal/pkg/services"
"github.com/zlgwzy/go-project-sample/api/proto"
"testing"
)
var configFile = flag.String("f", "proxy.yml", "set config file which viper will loading.")
func TestProductsService_Get(t *testing.T) {
flag.Parse()
service := new(services.MockProductsService)
service.On("Get", mock.AnythingOfType("uint64")).Return(func(ID uint64) (p *models.Product) {
return &models.Product{
ID: ID,
}
}, func(ID uint64) error {
return nil
})
server, err := CreateProductsServer(*configFile, service)
if err != nil {
t.Fatalf("create product server error,%+v", err)
}
// 表格驱动测试
tests := []struct {
name string
id uint64
expected uint64
}{
{"1+1", 1, 1},
{"2+3", 2, 2},
{"4+5", 3, 3},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := &proto.ProductGetRequest{
ID: test.id,
}
p, err := server.Get(context.Background(), req)
if err != nil {
t.Fatalf("product service get proudct error,%+v", err)
}
assert.Equal(t, test.expected, p.ID)
})
}
}
Makefile
编写Makefile
.PHONY: run
run:
go run ./cmd -f cmd/app.yml
.PHONY: wire
wire:
wire ./...
.PHONY: test
test:
go test -v ./... -f `pwd`/cmd/app.yml -covermode=count -coverprofile=dist/test/cover.out
.PHONY: build
build:
GOOS=linux GOARCH="amd64" go build ./cmd -o dist/sample5-linux-amd64
GOOS=darwin GOARCH="amd64" go build ./cmd -o dist/sample5-darwin-amd64
.PHONY: cover
cover:
go tool cover -html=dist/test/cover.out
.PHONY: mock
mock:
mockery --all --inpkg
.PHONY: lint
lint:
golint ./...
.PHONY: proto
proto:
protoc -I api/proto ./api/proto/products.proto --go_out=plugins=grpc:api/proto
docker: build
docker-compose -f docker/sample/docker-compose.yml up
- make run 运行项目
- make wire 生成依赖注入的代码
- make mock 生成mock对象
- make test 运行单元测试
- cover 查看测试用例覆盖度
- make build 编译代码
- make lint 静态代码检查
- make proto 生成grpc代码
- make docker 通过docker启动项目,包括依赖的数据
框架或库
比较喜欢和常用的几个框架或库
- Gin MVC库
- gorm ORM库
- viper 配置管理库
- zap 日志库
- grpc RPC库
- Cobar Command开发库
- Opentracing 调用链跟踪
- go-prometheus 服务监控
- wire 依赖注入