10.Go-Kratos
Go-Kratos使用
Kratos是一个由Bilibili开源的Go语言微服务框架,旨在为分布式应用提供高效、灵活、可扩展的解决方案。 下面将详细介绍Kratos的基本概念、特点和优势。 Kratos是一个轻量级的Go语言微服务框架,它提供了一套简单易用的API和工具,使得开发者能够轻松地构建和管理微服务应用。
1.环境准备
1.1安装依赖
安装 protoc:
- 到 protobuf release 页面,选择适合自己操作系统的文件包。
- 或者文档也可以看 grpc.io 官方安装文档: https://grpc.io/docs/protoc-installation/
安装 protoc-gen-go:
- 简单点可以直接用 go install 安装 :
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- 或者看文档:https://developers.google.com/protocol-buffers/docs/reference/go-generated
- 建议开启 GO111MODULE
go env -w GO111MODULE=on
wire
# 安装依赖
go get github.com/google/wire/cmd/wire@latest
# 生成所有proto源码、wire等等
go generate ./...
1.2安装kratos cli
go install github.com/go-kratos/kratos/cmd/kratos/v2@latest
- CLI 工具使用说明:CLI工具使用
2.DEMO启动
2.1拉取项目模版
使用 kratos new
命令创建 quickstart 项目:kratos new quickstart
如果拉取 github 上的项目模板失败,可以使用 -r 参数指定拉取项目模板地址.
比如拉取 gitee 上的模板:
kratos new quickstart -r https://gitee.com/go-kratos/kratos-layout.git
2.2生成相应代码
使用 go generate 命令生成相应代码
生成 proto 源码、wire 等等:go generate ./...
2.3运行项目
使用 kratos run
命令运行项目
fly@flydeMac-Pro > /Volumes/data/go/src/kratos-demo/quickstart > kratos run
2024/12/22 22:27:44 maxprocs: Leaving GOMAXPROCS=32: CPU quota undefined
DEBUG msg=config loaded: config.yaml format: yaml
INFO ts=2024-12-22T22:27:44+08:00 caller=http/server.go:330 service.id=flydeMac-Pro.lan service.name= service.version= trace.id= span.id= msg=[HTTP] server listening on: [::]:8000
INFO ts=2024-12-22T22:27:44+08:00 caller=grpc/server.go:212 service.id=flydeMac-Pro.lan service.name= service.version= trace.id= span.id= msg=[gRPC] server listening on: [::]:9000
2.4测试
fly@flydeMac-Pro > /Volumes/data/go/src/kratos-demo/quickstart > curl http://localhost:8000/helloworld/kratos
{"message":"Hello kratos"}
3.Kratos项目结构介绍
.
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── api // 下面维护了微服务使用的proto文件以及根据它们所生成的go文件
│ └── helloworld
│ └── v1
│ ├── error_reason.pb.go
│ ├── error_reason.proto
│ ├── error_reason.swagger.json
│ ├── greeter.pb.go
│ ├── greeter.proto
│ ├── greeter.swagger.json
│ ├── greeter_grpc.pb.go
│ └── greeter_http.pb.go
├── cmd // 整个项目启动的入口文件
│ └── server
│ ├── main.go
│ ├── wire.go // 我们使用wire来维护依赖注入
│ └── wire_gen.go
├── configs // 这里通常维护一些本地调试用的样例配置文件
│ └── config.yaml
├── generate.go
├── go.mod
├── go.sum
├── internal // 该服务所有不对外暴露的代码,通常的业务逻辑都在这下面,使用internal避免错误引用
│ ├── biz // 业务逻辑的组装层,类似 DDD 的 domain 层,data 类似 DDD 的 repo,而 repo 接口在这里定义,使用依赖倒置的原则。
│ │ ├── README.md
│ │ ├── biz.go
│ │ └── greeter.go
│ ├── conf // 内部使用的config的结构定义,使用proto格式生成
│ │ ├── conf.pb.go
│ │ └── conf.proto
│ ├── data // 业务数据访问,包含 cache、db 等封装,实现了 biz 的 repo 接口。我们可能会把 data 与 dao 混淆在一起,data 偏重业务的含义,它所要做的是将领域对象重新拿出来,我们去掉了 DDD 的 infra层。
│ │ ├── README.md
│ │ ├── data.go
│ │ └── greeter.go
│ ├── server // http和grpc实例的创建和配置
│ │ ├── grpc.go
│ │ ├── http.go
│ │ └── server.go
│ └── service // 实现了 api 定义的服务层,类似 DDD 的 application 层,处理 DTO 到 biz 领域实体的转换(DTO -> DO),同时协同各类 biz 交互,但是不应处理复杂逻辑
│ ├── README.md
│ ├── greeter.go
│ └── service.go
└── third_party // api 依赖的第三方proto
├── README.md
├── google
│ └── api
│ ├── annotations.proto
│ ├── http.proto
│ └── httpbody.proto
└── validate
├── README.md
└── validate.proto
3.1hello接口的实现
在第二小节中我们测试了一个helloworld接口,下面看一下具体实现过程:
- 在项目目录下的
api/helloworld/v1/greeter.proto
下
//定义一个grpc的方法,该方法接收HelloRequest返回是HelloReply
//同时定义一个option,代表同时生成http的接口 http方法:接口路径
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/helloworld/{name}"
};
}
}
// 输入
message HelloRequest {
string name = 1;
}
// 输出
message HelloReply {
string message = 1;
}
- 生成 Proto Client代码
# 可以直接通过 make 命令生成
make api
# 或使用 kratos cli 进行生成
kratos proto client api/helloworld/v1/greeter.proto
fly@flydeMac-Pro > /Volumes/data/go/src/kratos-demo/quickstart/api/helloworld/v1 > kratos proto client ./
proto: error_reason.proto
proto: greeter.proto
fly@flydeMac-Pro > /Volumes/data/go/src/kratos-demo/quickstart/api/helloworld/v1 > ll
total 80
-rw-r--r-- 1 fly admin 4.3K Dec 22 22:56 error_reason.pb.go
-rw-r--r-- 1 fly admin 290B Dec 22 22:19 error_reason.proto
-rw-r--r-- 1 fly admin 6.6K Dec 22 22:56 greeter.pb.go
-rw-r--r-- 1 fly admin 678B Dec 22 22:19 greeter.proto
-rw-r--r-- 1 fly admin 4.2K Dec 22 22:56 greeter_grpc.pb.go
-rw-r--r-- 1 fly admin 2.2K Dec 22 22:56 greeter_http.pb.go
-rw-r--r-- 1 fly admin 3.0K Dec 22 22:56 openapi.yaml
- 生成Proto Service代码
通过 proto 文件,可以直接生成对应的 Service 实现代码:
//使用 -t 指定生成目录
kratos proto server api/helloworld/v1/greeter.proto -t internal/service
internal/service/greeter.go
package service
import (
"context"
pb "quickstart/api/helloworld/v1"
)
type GreeterService struct {
pb.UnimplementedGreeterServer
}
func NewGreeterService() *GreeterService {
return &GreeterService{}
}
func (s *GreeterService) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{}, nil
}
internal/service
中需要实现SayHello
如下:
package service
import (
"context"
v1 "quickstart/api/helloworld/v1"
"quickstart/internal/biz"
)
type GreeterService struct {
v1.UnimplementedGreeterServer
uc *biz.GreeterUsecase
}
func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {
return &GreeterService{uc: uc}
}
func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {
g, err := s.uc.CreateGreeter(ctx, &biz.Greeter{Hello: in.Name})
if err != nil {
return nil, err
}
return &v1.HelloReply{Message: "Hello " + g.Hello}, nil
}
4.Kratos框架的DDD思想
4.1biz层的接口
在internal/biz/greeter.go
中需要定义一个GreeterRepo
type GreeterRepo interface {
Save(context.Context, *Greeter) (*Greeter, error)
...
}
GreeterRepo
可以简单理解为,你对于数据库有哪些操作,比如这里在接口中定义了一个save方法。这个方法我们需要在data层进行实现
4.2data层的实现
在internal/data/greeter.go
中需要对biz层GreeterRepo接口进行实现
...
import (
"context"
"quickstart/internal/biz"
...
)
...
type greeterRepo struct {
data *Data
log *log.Helper
}
// NewGreeterRepo .
func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {
return &greeterRepo{
data: data,
log: log.NewHelper(logger),
}
}
func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
return g, nil
}
4.3biz层中的 依赖控制反转
我们会发现,biz层中并不依赖data层。也就是说biz不需要关心data层用的是什么数据库或是调用的其他三方接口。
在internal/biz/greeter.go
中
// Greeter is a Greeter model.
type Greeter struct {
Hello string
}
type GreeterRepo interface {
Save(context.Context, *Greeter) (*Greeter, error)
Update(context.Context, *Greeter) (*Greeter, error)
FindByID(context.Context, int64) (*Greeter, error)
ListByHello(context.Context, string) ([]*Greeter, error)
ListAll(context.Context) ([]*Greeter, error)
}
type GreeterUsecase struct {
repo GreeterRepo //将GreeterRepo接口包含近GreeterUsecase结构体
log *log.Helper
}
func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {
return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}
func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
return uc.repo.Save(ctx, g)
}
4.4data层连接数据库
在internal/data/greeter.go
中定义的greeterRepo结构体
type greeterRepo struct {
data *Data //其中包含了一个data结构体的指针
log *log.Helper
}
internal/data/data.go
中官方记录了一个todo,表明这里可以嵌入相关的db client
// Data .
type Data struct {
// TODO wrapped database client
}
// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
cleanup := func() {
log.NewHelper(logger).Info("closing the data resources")
}
return &Data{}, cleanup, nil
}
configs/config.yaml
编写配置文件
server:
http:
addr: 0.0.0.0:8000
timeout: 1s
grpc:
addr: 0.0.0.0:9000
timeout: 1s
data:
database:
driver: sqlite3
source: ./test.db?_fk=1
redis:
addr: 127.0.0.1:6379
read_timeout: 0.2s
write_timeout: 0.2s
internal/conf/conf.proto
定义配置文件proto映射
syntax = "proto3";
package kratos.api;
option go_package = "kubecit/internal/conf;conf";
import "google/protobuf/duration.proto";
message Bootstrap {
Server server = 1;
Data data = 2;
}
message Server {
message HTTP {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
}
message GRPC {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
}
HTTP http = 1;
GRPC grpc = 2;
}
message Data {
message Database {
string driver = 1;
string source = 2;
}
message Redis {
string network = 1;
string addr = 2;
google.protobuf.Duration read_timeout = 3;
google.protobuf.Duration write_timeout = 4;
}
Database database = 1;
Redis redis = 2;
}
internal/data/data.go
初始化dbclient
// Data contains config and db client
type Data struct {
conf *conf.Data
db *ent.Client
}
// NewData 构造方法,初始化了数据库 client
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
cleanup := func() {
log.NewHelper(logger).Info("closing the data resources")
}
entClient, err := ent.Open(c.Database.Driver, c.Database.Source)
if err != nil {
log.Fatalf("fail to open connection to db,%s", err)
}
if err := entClient.Schema.Create(context.Background()); err != nil {
log.Fatalf("fail to create schema,%s", err)
}
return &Data{
conf: c,
db: entClient,
}, cleanup, nil
}
internal/data/user.go
使用dbclient操作数据库
package data
import (
"context"
"fmt"
"github.com/go-kratos/kratos/v2/log"
"kubecit/internal/biz"
)
// userRepo 实现了 biz 层 UserRepo interface
type userRepo struct {
data *Data
log *log.Helper
}
// NewUserRepo 用户数据仓库构造方法
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
return &userRepo{
data: data,
log: log.NewHelper(logger),
}
}
// Create 在用户表插入一个用户,注意返回值 为 biz.User
func (u *userRepo) Create(ctx context.Context, user *biz.User) (*biz.User, error) {
userEnt, err := u.data.db.User.Create().SetName(user.Username).SetAge(1).SetPassword(user.Password).Save(ctx)
if err != nil {
fmt.Println(err)
}
return &biz.User{
Username: userEnt.Name,
Password: userEnt.Password,
Age: userEnt.Age,
}, nil
}
// Create 在用户表删除一个用户,注意返回值 为 biz.User
func (u *userRepo) Delete(ctx context.Context, id int) error {
return u.data.db.User.DeleteOneID(id).Exec(ctx)
}
// List 列出用户表所有用户
func (u *userRepo) List(ctx context.Context) ([]*biz.User, error) {
users, err := u.data.db.User.Query().All(ctx)
if err != nil {
return nil, err
}
var userResults []*biz.User
for _, user := range users {
userResults = append(userResults, &biz.User{
Id: user.ID,
Username: user.Name,
Password: user.Password,
Age: user.Age,
})
}
return userResults, nil
}
4.5biz层调用data层
internal/biz/user.go
编写业务逻辑
package biz
import (
"context"
"github.com/go-kratos/kratos/v2/log"
)
type User struct {
Id int
Username string
Password string
Age int
}
// UserRepo 接口,定义了 data 层需要提供的能力,此接口实现者为 data/user.go 文件中的 userRepo
//
//go:generate mockgen -destination=../mocks/mrepo/user.go -package=mrepo . UserRepo
type UserRepo interface {
Create(context.Context, *User) (*User, error)
List(ctx context.Context) ([]*User, error)
Delete(ctx context.Context, id int) error
}
// UserUsecase 用户领域结构体,可以包含多个与用户业务相关的 repo
type UserUsecase struct {
repo UserRepo
log *log.Helper
}
// NewUserUsecase 用户领域构造方法
func NewUserUsecase(repo UserRepo, logger log.Logger) *UserUsecase {
return &UserUsecase{repo: repo, log: log.NewHelper(logger)}
}
// RegisterUser 注册一个用户
func (u *UserUsecase) RegisterUser(ctx context.Context, user *User) (*User, error) {
userResult, err := u.repo.Create(ctx, user)
if err != nil {
return nil, err
}
return userResult, nil
}
// UserList 列出所有用户
func (u *UserUsecase) UserList(ctx context.Context) ([]*User, error) {
userResult, err := u.repo.List(ctx)
if err != nil {
return nil, err
}
return userResult, nil
}
小结: 在biz层写业务逻辑,吧对数据库层的操作都封装到repo接口中,并在data层实现这个repo接口。通过wire将repo和usecase进行依赖注入,实现控制反转!这样我们整个kratos项目就可以奔跑起来了。
5.Kratos中使用wire
我们先看一下生成出来的cmd/kubecit/wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
"kubecit/internal/biz"
"kubecit/internal/conf"
"kubecit/internal/data"
"kubecit/internal/server"
"kubecit/internal/service"
)
import (
_ "go.uber.org/automaxprocs"
)
// Injectors from wire.go:
// wireApp init kratos application.
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {
dataData, cleanup, err := data.NewData(confData, logger)
if err != nil {
return nil, nil, err
}
greeterRepo := data.NewGreeterRepo(dataData, logger)
greeterUsecase := biz.NewGreeterUsecase(greeterRepo, logger)
userRepo := data.NewUserRepo(dataData, logger)
userUsecase := biz.NewUserUsecase(userRepo, logger)
greeterService := service.NewGreeterService(greeterUsecase, userUsecase)
grpcServer := server.NewGRPCServer(confServer, greeterService, logger)
httpServer := server.NewHTTPServer(confServer, greeterService, logger)
app := newApp(logger, grpcServer, httpServer)
return app, func() {
cleanup()
}, nil
}
dataData, cleanup, err := data.NewData(confData, logger)
- 这里调用了我们4.4小节中我们初始化dbclient的方法,拿到一个dbclient实例
userRepo := data.NewUserRepo(dataData, logger)
- 这里调用了我们4.4小节中我们初始化userRepo的方法,拿到一个data层userRepo结构体的实例
- 该实例实现了biz层的UserRepo interface,即对数据库的一些操作进行了封装(创建、查询、删除用户)
userUsecase := biz.NewUserUsecase(userRepo, logger)
- 这里调用了我们4.5小节中我们初始化Usecase的方法,拿到一个用户领域结构体的实例
- 该实例封装了biz层的UserRepo interface,可以让我们在不依赖data层的情况下调用data层的UserRepo及其封装的对数据库的一些操作。
总体来讲,wire的工作是帮我们调用了各层的构造函数,以及构造函数所依赖的子对象的构造函数。帮我们进行了依赖的管理
5.1wire的Provider如何声明?
wire详细使用方法可以参考我的另一篇文章go-Wire,这里不再赘述。我们简单看一下wire如和在kratos中帮我们梳理的依赖关系(即如何管理Repository和UseCase)
在将需要的data层repo、biz层repo interface、usercase定义好后。
在data层:internal/data/data.go
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewGreeterRepo, NewUserRepo)
//如果他要新增其他repo,这里需要添加repo声明
internal/biz/biz.go
// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(NewGreeterUsecase, NewUserUsecase)
//如果要新增其他usecase,这里需要添加usecase声明
internal/service/service.go
// ProviderSet is service providers.
var ProviderSet = wire.NewSet(NewGreeterService)
internal/service/greeter.go
service层调用biz层
// GreeterService is a greeter service.
type GreeterService struct {
v1.UnimplementedGreeterServer
uc *biz.GreeterUsecase
userCase *biz.UserUsecase
//如果要新增其他领域接口,这里需要嵌入其他biz层usecase
}
// NewGreeterService new a greeter service.
func NewGreeterService(uc *biz.GreeterUsecase, userCase *biz.UserUsecase, clusterCase) *GreeterService {
return &GreeterService{uc: uc, userCase: userCase}
//如果要新增其他领域接口,这里需要传入并添加其他biz层usecase
}
// SayHello implements helloworld.GreeterServer.
func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {
g, err := s.uc.CreateGreeter(ctx, &biz.Greeter{Hello: in.Name})
if err != nil {
return nil, err
}
return &v1.HelloReply{Message: "Hello niko" + g.Hello}, nil
}
// UserRegister register a user with username and password
func (s *GreeterService) UserRegister(ctx context.Context, in *v1.UserRegisterRequest) (*v1.UserRegisterResponse, error) {
fmt.Println(in.Username, in.Password)
_, err := s.userCase.RegisterUser(ctx, &biz.User{
Username: in.Username,
Password: in.Password,
})
if err != nil {
return nil, err
}
return &v1.UserRegisterResponse{Result: "success"}, nil
}
func (s *GreeterService) UserList(ctx context.Context, in *v1.Empty) (*v1.UserListResponse, error) {
users, err := s.userCase.UserList(ctx)
if err != nil {
return nil, err
}
userRes := []*v1.User{}
for _, v := range users {
userRes = append(userRes, &v1.User{
Username: v.Username,
Password: v.Password,
})
}
return &v1.UserListResponse{Users: userRes}, nil
}
cmd/kubecit/wire.go
//go:build wireinject
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"kubecit/internal/biz"
"kubecit/internal/conf"
"kubecit/internal/data"
"kubecit/internal/server"
"kubecit/internal/service"
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/wire"
)
// wireApp init kratos application.
func wireApp(*conf.Server, *conf.Data, log.Logger) (*kratos.App, func(), error) {
panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}
然后执行wire
即可:
fly@flydeMac-Pro > /Volumes/data/go/src/kubecit/cmd/kubecit > master ✚ > wire
wire: kubecit/cmd/kubecit: wrote /Volumes/data/go/src/kubecit/cmd/kubecit/wire_gen.go
6.总结
6.1Kratos
每一层通常代表了该层的核心责任。例如:
- Data 层:定义数据访问方法,如 Create, Update, Delete 等,这些方法集中管理所有与数据存储相关的操作。
- Biz 层:封装业务逻辑,定义如 Register, Login, GetUser 等方法,明确了业务流程和规则。
- Service 层:对外暴露服务,提供具体的 API,定义了如何与外部交互。
- Server 层: http和grpc实例的创建和配置。
- 按照上面的顺序:Kratos程序初始化通过Wire实现从上到下,用户访问则是从下到上。
6.2Wire
Wire
帮助你自动管理依赖注入,避免了手动传递依赖的复杂性。Repository
通常用于封装数据访问逻辑,而 UseCase 处理具体的业务逻辑。Wire
可以帮助你通过依赖注入将它们有效地组合起来。- 通过
wire.Build
自动生成代码,将各个组件之间的依赖关系注入和管理起来,降低耦合,提高代码的可维护性。 - 需要正确理解如何使用 Wire 以及 repo 和 usecase 的含义。
Last updated 23 Dec 2024, 01:10 +0800 .