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 依赖注入