本章来看看demo项目的整体结构。
目录结构
├─api # 对外接口 ├─cmd # main ├─configs # 配置 ├─internal │ ├─dao #数据访问 │ ├─di #依赖注入 │ ├─model #业务结构体的声明 │ ├─server #grpc、http初始化 │ │ ├─grpc │ │ └─http │ └─service #业务逻辑处理 └─test
官方文档解释
├── CHANGELOG.md ├── OWNERS ├── README.md ├── api # api目录为对外保留的proto文件及生成的pb.go文件 │ ├── api.bm.go │ ├── api.pb.go # 通过go generate生成的pb.go文件 │ ├── api.proto │ └── client.go ├── cmd │ └── main.go # cmd目录为main所在 ├── configs # configs为配置文件目录 │ ├── application.toml # 应用的自定义配置文件,可能是一些业务开关如:useABtest = true │ ├── db.toml # db相关配置 │ ├── grpc.toml # grpc相关配置 │ ├── http.toml # http相关配置 │ ├── memcache.toml # memcache相关配置 │ └── redis.toml # redis相关配置 ├── go.mod ├── go.sum └── internal # internal为项目内部包,包括以下目录: │ ├── dao # dao层,用于数据库、cache、MQ、依赖某业务grpc|http等资源访问 │ │ ├── dao.bts.go │ │ ├── dao.go │ │ ├── db.go │ │ ├── mc.cache.go │ │ ├── mc.go │ │ └── redis.go │ ├── di # 依赖注入层 采用wire静态分析依赖 │ │ ├── app.go │ │ ├── wire.go # wire 声明 │ │ └── wire_gen.go # go generate 生成的代码 │ ├── model # model层,用于声明业务结构体 │ │ └── model.go │ ├── server # server层,用于初始化grpc和http server │ │ ├── grpc # grpc层,用于初始化grpc server和定义method │ │ │ └── server.go │ │ └── http # http层,用于初始化http server和声明handler │ │ └── server.go │ └── service # service层,用于业务逻辑处理,且为方便http和grpc共用方法,建议入参和出参保持grpc风格,且使用pb文件生成代码 │ └── service.go └── test # 测试资源层 用于存放测试相关资源数据 如docker-compose配置 数据库初始化语句等 └── docker-compose.yaml
下面简单看看各层目录,api应该是最复杂的部分,其他的都很好看懂。
├── api # api目录为对外保留的proto文件及生成的pb.go文件 │ ├── api.bm.go │ ├── api.pb.go # 通过go generate生成的pb.go文件 │ ├── api.proto │ └── client.go
api目录主要为对外接口目录、api.bm.go 、apb.pb.go 可以通过kartos tool生成(kratos tool可以基于proto生成http&grpc代码,生成缓存回源代码,生成memcache执行代码,生成swagger文档等工具集) bm、 pb 分别为http和grpc的接口。
C:\server\src\go\src\demp\api>kratos tool protoc --grpc --bm api.proto go get -u github.com/bilibili/kratos/tool/kratos-protoc protoc: 安装成功! 2019/12/23 17:48:51 protoc --proto_path=C:\server\src\go/src --proto_path=C:\server\src\go/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/third_party --proto_path=C:\server\src\go\src\demp\api --bm_out=:. api.proto 2019/12/23 17:48:52 protoc --proto_path=C:\server\src\go/src --proto_path=C:\server\src\go/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/third_party --proto_path=C:\server\src\go\src\demp\api --gofast_out=plugins=grpc:. api.proto 2019/12/23 17:48:53 generate api.proto success.
api.bm.go 为http的对外接口, BM server即blademaster为热度http框架gin的裁剪.去除了gin中不需要的部分逻辑,
api目录主要为对外目录、api.bm.go 、apb.pb.go 可以通过kartos tool生成(kratos tool可以基于proto生成http&grpc代码,生成缓存回源代码,生成memcache执行代码,生成swagger文档等工具集) bm、 pb 分别为http和grpc的接口。
像上篇文章,protoc 没装 不能运行的情况下,其实可以用kratos tool 来生成 对应go文件的。
C:\server\src\go\src\demp\api>kratos tool protoc --grpc --bm api.proto I:. api.proto client.go 没有子文件夹 I:\VSProject\go\src\demo\api>kratos tool protoc --grpc --bm api.proto go get -u github.com/bilibili/kratos/tool/kratos-protoc protoc: 安装成功! 2019/12/24 21:13:18 go get -u github.com/bilibili/kratos/tool/protobuf/protoc-gen-bm go: downloading github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 go: extracting github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 go: finding google.golang.org/genproto latest go: finding github.com/siddontang/go latest go: downloading google.golang.org/genproto v0.0.0-20191223191004-3caeed10a8bf go: extracting google.golang.org/genproto v0.0.0-20191223191004-3caeed10a8bf 2019/12/24 21:13:37 protoc --proto_path=I:\VSProject\go/src --proto_path=I:\VSProject\go/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/third_party --proto_path=I:\VSProject\go\src\demo\api --bm_out=:. api.proto 2019/12/24 21:13:37 go get -u github.com/gogo/protobuf/protoc-gen-gofast go: finding github.com/gogo/protobuf v1.3.1 go: downloading github.com/gogo/protobuf v1.3.1 go: extracting github.com/gogo/protobuf v1.3.1 2019/12/24 21:13:46 protoc --proto_path=I:\VSProject\go/src --proto_path=I:\VSProject\go/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/third_party --proto_path=I:\VSProject\go\src\demo\api --gofast_out=plugins=grpc:. api.proto 2019/12/24 21:13:47 generate api.proto success. I:\VSProject\go\src\demo\api>tree /f I:. api.bm.go api.pb.go api.proto client.go
但这样还是不够运行,错误是缺少di.InitApp(), 对比上次笔记(一)的正常项目,会发现还少了一个wire_gen.go文件
I:\VSProject\go\src\demo\internal\di>kratos run # command-line-arguments .\main.go:21:23: undefined: di.InitApp panic: exit status 2 goroutine 1 [running]: main.runAction(0xc000102160, 0x0, 0xc0000ee170) I:/VSProject/go/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/tool/kratos/run.go:25 +0x36e github.com/urfave/cli.HandleAction(0x603080, 0x65fdc8, 0xc000102160, 0xc000102160, 0x0) I:/VSProject/go/pkg/mod/github.com/urfave/cli@v1.22.2/app.go:523 +0xc5 github.com/urfave/cli.Command.Run(0x64c994, 0x3, 0x0, 0x0, 0xc0000ee020, 0x1, 0x1, 0x650d90, 0xa, 0x0, ...) I:/VSProject/go/pkg/mod/github.com/urfave/cli@v1.22.2/command.go:174 +0x523 github.com/urfave/cli.(*App).Run(0xc0000e8000, 0xc0000044a0, 0x2, 0x2, 0x0, 0x0) I:/VSProject/go/pkg/mod/github.com/urfave/cli@v1.22.2/app.go:276 +0x72c main.main() I:/VSProject/go/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/tool/kratos/main.go:57 +0x3f7
官方文档解释这个文件也是生成出来的、尝试后,发现go generate可以生成它。
I:\VSProject\go\src\demo\internal\di>go generate go get -u github.com/google/wire/cmd/wire go: finding golang.org/x/tools latest wire: 安装成功! wire: demo/internal/di: wrote I:\VSProject\go\src\demo\internal\di\wire_gen.go I:\VSProject\go\src\demo\internal\di>
后来发现其实前面的api路径下的go文件也可以用go generate生成。
go generate go get -u github.com/bilibili/kratos/tool/kratos-protoc protoc: 安装成功! 2019/12/24 21:42:31 protoc --proto_path=I:\VSProject\go/src --proto_path=I:\VSProject\go/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191224125553-6e1180f53a8e/third_party --proto_path=I:\VSProject\go\src\demo\api --bm_out=:. api.proto 2019/12/24 21:42:31 protoc --proto_path=I:\VSProject\go/src --proto_path=I:\VSProject\go/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191224125553-6e1180f53a8e/third_party --proto_path=I:\VSProject\go\src\demo\api --gofast_out=plugins=grpc:. api.proto 2019/12/24 21:42:31 generate api.proto success. I:\VSProject\go\src\demo\api>
回来看两个api下的两个go接口:
api.bm.go 为BM server的对外接口, BM server即blademaster为热度http框架gin的裁剪.去除了gin中不需要的部分逻辑,
api.pb.go 为grpc的对外接口,应该就是生成的protocbuf 文件。
看看熟悉的api.bm.go
// DemoBMServer is the server API for Demo service. type DemoBMServer interface { Ping(ctx context.Context, req *google_protobuf1.Empty) (resp *google_protobuf1.Empty, err error) SayHello(ctx context.Context, req *HelloReq) (resp *google_protobuf1.Empty, err error) SayHelloURL(ctx context.Context, req *HelloReq) (resp *HelloResp, err error) } var DemoSvc DemoBMServer func demoPing(c *bm.Context) { p := new(google_protobuf1.Empty) if err := c.BindWith(p, binding.Default(c.Request.Method, c.Request.Header.Get("Content-Type"))); err != nil { return } resp, err := DemoSvc.Ping(c, p) c.JSON(resp, err) } func demoSayHello(c *bm.Context) { p := new(HelloReq) if err := c.BindWith(p, binding.Default(c.Request.Method, c.Request.Header.Get("Content-Type"))); err != nil { return } resp, err := DemoSvc.SayHello(c, p) c.JSON(resp, err) } func demoSayHelloURL(c *bm.Context) { p := new(HelloReq) if err := c.BindWith(p, binding.Default(c.Request.Method, c.Request.Header.Get("Content-Type"))); err != nil { return } resp, err := DemoSvc.SayHelloURL(c, p) c.JSON(resp, err) } // RegisterDemoBMServer Register the blademaster route func RegisterDemoBMServer(e *bm.Engine, server DemoBMServer) { DemoSvc = server e.GET("/demo.service.v1.Demo/Ping", demoPing) e.GET("/demo.service.v1.Demo/SayHello", demoSayHello) e.GET("/abc/say_hello", demoSayHelloURL) }
这个文件会生成以bm上下文为参数的三个接口函数,这些三个接口函数分别是在api.proto里面定义的grpc接口
option go_package = "api"; option (gogoproto.goproto_getters_all) = false; service Demo { rpc Ping (.google.protobuf.Empty) returns (.google.protobuf.Empty); rpc SayHello (HelloReq) returns (.google.protobuf.Empty); rpc SayHelloURL(HelloReq) returns (HelloResp) { option (google.api.http) = { get:"/kratos-demo/say_hello" }; }; } message HelloReq { string name = 1 [(gogoproto.moretags)='form:"name" validate:"required"']; } message HelloResp { string Content = 1 [(gogoproto.jsontag) = 'content']; }
RegisterDemoBMServer() 会将这三个接口函数注册到bm 引擎的路由上。 可以看到生成的三个接口只是对请求的消息做了简单的校验,然后调用service下的service.go 实现这三个的接口业务。
BindWith() 简单看了下 其实就是校验数据格式是否正确。bind.Default()使用默认校验方式 。默认校验方式失败会返回400。
├── cmd │ └── main.go # cmd目录为main所在
main 函数路径 整个服务入口
也没干什么, 初始化日志、paladin配置包初始化、初始化依赖和服务、跑个循环等待信号退出。
flag.Parse() log.Init(nil) // debug flag: log.dir={path} defer log.Close() log.Info("demo start") paladin.Init() _, closeFunc, err := di.InitApp() if err != nil { panic(err) } c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) for { s := <-c log.Info("get a signal %s", s.String()) switch s { case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: closeFunc() log.Info("demo exit") time.Sleep(time.Second) return case syscall.SIGHUP: default: return } }
配置目录、demo使用的toml格式。
├── configs # configs为配置文件目录 │ ├── application.toml # 应用的自定义配置文件,可能是一些业务开关如:useABtest = true │ ├── db.toml # db相关配置 │ ├── grpc.toml # grpc相关配置 │ ├── http.toml # http相关配置 │ ├── memcache.toml # memcache相关配置 │ └── redis.toml # redis相关配置
简单看一个http的
[Server] addr = "0.0.0.0:8000" timeout = "1s"
│ ├── dao # dao层,用于数据库、cache、MQ、依赖某业务grpc|http等资源访问 │ │ ├── dao.bts.go │ │ ├── dao.go │ │ ├── db.go │ │ ├── mc.cache.go │ │ ├── mc.go │ │ └── redis.go
我目前只了解redis、就看看redis了,是对redis的操作封装。
package dao import ( "context" "github.com/bilibili/kratos/pkg/cache/redis" "github.com/bilibili/kratos/pkg/conf/paladin" "github.com/bilibili/kratos/pkg/log" ) func NewRedis() (r *redis.Redis, err error) { var cfg struct { Client *redis.Config } if err = paladin.Get("redis.toml").UnmarshalTOML(&cfg); err != nil { return } r = redis.NewRedis(cfg.Client) return } func (d *dao) PingRedis(ctx context.Context) (err error) { if _, err = d.redis.Do(ctx, "SET", "ping", "pong"); err != nil { log.Error("conn.Set(PING) error(%v)", err) } return }
NewRedis() 返回的的应该是redis的连接池,可通过Get()方法来取条连接,kratos/cache/redis里面对它做了进一步的封装。
type Redis struct { pool *Pool conf *Config } // Get gets a connection. The application must close the returned connection. // This method always returns a valid connection so that applications can defer // error handling to the first use of the connection. If there is an error // getting an underlying connection, then the connection Err, Do, Send, Flush // and Receive methods return that error. func (p *Pool) Get(ctx context.Context) Conn { c, err := p.Slice.Get(ctx) if err != nil { return errorConnection{err} } c1, _ := c.(Conn) return &pooledConnection{p: p, c: c1.WithContext(ctx), rc: c1, now: beginTime} } // Close releases the resources used by the pool. func (p *Pool) Close() error { return p.Slice.Close() }
│ ├── di # 依赖注入层 采用wire静态分析依赖 │ │ ├── app.go │ │ ├── wire.go # wire 声明 │ │ └── wire_gen.go # go generate 生成的代码
使用了google wire静态分析依赖,它是golang的一个依赖注入解决的工具,这个工具能够自动生成类的依赖关系。 执行wire命令,会读取到wire.NewSet里面的ProviderSet,通过分析各个函数的参数和返回值,来自行解决依赖,可以生成wire_gen.go
// +build wireinject // The build tag makes sure the stub is not built in the final build. package di import ( pb "demo/api" "demo/internal/dao" "demo/internal/server/grpc" "demo/internal/server/http" "demo/internal/service" "github.com/google/wire" ) var daoProvider = wire.NewSet(dao.New, dao.NewDB, dao.NewRedis, dao.NewMC) var serviceProvider = wire.NewSet(service.New, wire.Bind(new(pb.DemoServer), new(*service.Service))) func InitApp() (*App, func(), error) { panic(wire.Build(daoProvider, serviceProvider, http.New, grpc.New, NewApp)) }
│ ├── model # model层,用于声明业务结构体 │ │ └── model.go
package model // Kratos hello kratos. type Kratos struct { Hello string } type Article struct { ID int64 Content string Author string }
│ ├── server # server层,用于初始化grpc和http server │ │ ├── grpc # grpc层,用于初始化grpc server和定义method │ │ │ └── server.go │ │ └── http # http层,用于初始化http server和声明handler │ │ └── server.go
var svc pb.DemoServer // New new a bm server. func New(s pb.DemoServer) (engine *bm.Engine, err error) { var ( hc struct { Server *bm.ServerConfig } ) if err = paladin.Get("http.toml").UnmarshalTOML(&hc); err != nil { if err != paladin.ErrNotExist { return } err = nil } svc = s engine = bm.DefaultServer(hc.Server) pb.RegisterDemoBMServer(engine, s) initRouter(engine) err = engine.Start() return } func initRouter(e *bm.Engine) { e.Ping(ping) g := e.Group("/demo") { g.GET("/start", howToStart) } } func ping(ctx *bm.Context) { if _, err := svc.Ping(ctx, nil); err != nil { log.Error("ping error(%v)", err) ctx.AbortWithStatus(http.StatusServiceUnavailable) } } // example for http request handler. func howToStart(c *bm.Context) { k := &model.Kratos{ Hello: "Golang 大法好 !!!", } c.JSON(k, nil) }
│ └── service # service层,用于业务逻辑处理,且为方便http和grpc共用方法,建议入参和出参保持grpc风格,且使用pb文件生成代码 │ └── service.go
如api层所述、简单实现pb定义的几个接口业务逻辑。
// Service service. type Service struct { ac *paladin.Map dao dao.Dao } // New new a service and return. func New(d dao.Dao) (s *Service, err error) { s = &Service{ ac: &paladin.TOML{}, dao: d, } err = paladin.Watch("application.toml", s.ac) return } // SayHello grpc demo func. func (s *Service) SayHello(ctx context.Context, req *pb.HelloReq) (reply *empty.Empty, err error) { reply = new(empty.Empty) fmt.Printf("hello %s", req.Name) return } // SayHelloURL bm demo func. func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) { reply = &pb.HelloResp{ Content: "hello " + req.Name, } fmt.Printf("hello url %s", req.Name) return } // Ping ping the resource. func (s *Service) Ping(ctx context.Context, e *empty.Empty) (*empty.Empty, error) { return &empty.Empty{}, s.dao.Ping(ctx) }
体感整体结构层次分的很清晰,而且kratos 框架本身就包含很多工具,如果使用起来开发,感觉舒适度会更上一层楼,期待用上的那一天。