Kratos是一个由Bilibili开源的Go语言微服务框架,旨在为分布式应用提供高效、灵活、可扩展的解决方案。 下面将详细介绍Kratos的基本概念、特点和优势。 Kratos是一个轻量级的Go语言微服务框架,它提供了一套简单易用的API和工具,使得开发者能够轻松地构建和管理微服务应用。

1.环境准备

1.1安装依赖

安装 protoc:

安装 protoc-gen-go:

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

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
这里看到分别启动了8000和9000端口,这里分别是http和grpc协议

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接口,下面看一下具体实现过程:

  1. 在项目目录下的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;
}
  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

  1. 生成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
}
调用过程client > server层 > service层 > biz层 > data 层,后面会详细讲到

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
}
这里注意返回值,为biz层的结构体。也就是说data层会引用并依赖biz层

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)
}
这样的好处是,后期如果data层调用的db或接口作变更,biz层的代码不需要变动。

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声明
在biz层:internal/biz/biz.go
// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(NewGreeterUsecase, NewUserUsecase)
//如果要新增其他usecase,这里需要添加usecase声明
在service层:internal/service/service.go
// ProviderSet is service providers.
var ProviderSet = wire.NewSet(NewGreeterService)
internal/service/greeter.goservice层调用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 . history