本章来看看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                     # 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:serversrcgosrcdempapi>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:serversrcgo/src --proto_path=C:serversrcgo/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/third_party --proto_path=C:serversrcgosrcdempapi --bm_out=:. api.proto
2019/12/23 17:48:52 protoc --proto_path=C:serversrcgo/src --proto_path=C:serversrcgo/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/third_party --proto_path=C:serversrcgosrcdempapi --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:serversrcgosrcdempapi>kratos tool protoc --grpc --bm api.proto
I:.
    api.proto
    client.go

没有子文件夹

I:VSProjectgosrcdemoapi>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:VSProjectgo/src --proto_path=I:VSProjectgo/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/third_party --proto_path=I:VSProjectgosrcdemoapi --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:VSProjectgo/src --proto_path=I:VSProjectgo/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191216053608-e8e05452b3b0/third_party --proto_path=I:VSProjectgosrcdemoapi --gofast_out=plugins=grpc:. api.proto
2019/12/24 21:13:47 generate api.proto success.

I:VSProjectgosrcdemoapi>tree /f
I:.
    api.bm.go
    api.pb.go
    api.proto
    client.go

但这样还是不够运行,错误是缺少di.InitApp(), 对比上次笔记(一)的正常项目,会发现还少了一个wire_gen.go文件

I:VSProjectgosrcdemointernaldi>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:VSProjectgosrcdemointernaldi>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:VSProjectgosrcdemointernaldiwire_gen.go

I:VSProjectgosrcdemointernaldi>

后来发现其实前面的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:VSProjectgo/src --proto_path=I:VSProjectgo/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191224125553-6e1180f53a8e/third_party --proto_path=I:VSProjectgosrcdemoapi --bm_out=:. api.proto
2019/12/24 21:42:31 protoc --proto_path=I:VSProjectgo/src --proto_path=I:VSProjectgo/pkg/mod/github.com/bilibili/kratos@v0.3.2-0.20191224125553-6e1180f53a8e/third_party --proto_path=I:VSProjectgosrcdemoapi --gofast_out=plugins=grpc:. api.proto
2019/12/24 21:42:31 generate api.proto success.

I:VSProjectgosrcdemoapi>

回来看两个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

├── 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
		}
	}

configs

配置目录、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                 # 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

│   ├── 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层,用于声明业务结构体
│   │   └── model.go
package model

// Kratos hello kratos.
type Kratos struct {
	Hello string
}

type Article struct {
	ID int64
	Content string
	Author string
}

server

│   ├── 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             # 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 框架本身就包含很多工具,如果使用起来开发,感觉舒适度会更上一层楼,期待用上的那一天。

点赞(1740)

评论列表共有 0 条评论

立即
投稿
返回
顶部