0%

kratos 源码分析 02:kratos-layout 模板项目

上篇文章我们已经快速使用了 kratos CLI 命令行工具来创建一个微服务项目,我们也了解到,kratos CLI 是基于 kratos-layout 这个模版项目来生成初始项目代码的,这篇文章我们将详细分析下这个 kratos-layout 模板项目。

项目总览

首先我们来看下 kratos-layout 的完整目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
kratos-layout/
├── Dockerfile # 容器构建文件
├── Makefile # 构建、代码生成等自动化命令
├── openapi.yaml # 自动生成的 OpenAPI/Swagger 文档
├── api/ # Protobuf API 定义及生成的代码
│ └── helloworld/v1/
│ ├── greeter.proto # Greeter 服务 Proto 定义
│ ├── greeter.pb.go # Protobuf 消息类型(自动生成)
│ ├── greeter_grpc.pb.go # gRPC 服务端/客户端代码(自动生成)
│ ├── greeter_http.pb.go # HTTP 路由注册代码(自动生成)
│ ├── error_reason.proto # 错误枚举定义
│ └── error_reason.pb.go # 错误枚举代码(自动生成)
├── cmd/ # 启动入口
│ └── server/
│ ├── main.go # 主入口
│ ├── wire.go # Wire 依赖注入声明
│ └── wire_gen.go # Wire 生成代码
├── configs/ # 配置文件
│ └── config.yaml
├── internal/ # 内部实现(不对外暴露)
│ ├── biz/ # 业务逻辑层(DDD domain 层)
│ │ ├── biz.go # Wire ProviderSet
│ │ └── greeter.go # Greeter 用例 + Repo 接口定义
│ ├── conf/ # 配置结构定义
│ │ ├── conf.proto # Bootstrap/Server/Data 配置结构
│ │ └── conf.pb.go # 生成的 Go 配置结构体
│ ├── data/ # 数据访问层(DDD repo 实现)
│ │ ├── data.go # Wire ProviderSet + Data 初始化
│ │ └── greeter.go # GreeterRepo 实现
│ ├── server/ # HTTP/gRPC Server 创建
│ │ ├── server.go # Wire ProviderSet
│ │ ├── http.go # NewHTTPServer
│ │ └── grpc.go # NewGRPCServer
│ └── service/ # 服务层(DDD application 层)
│ ├── service.go # Wire ProviderSet
│ └── greeter.go # GreeterService 实现
└── third_party/ # 第三方 Proto 定义
├── errors/ # 错误 Proto
├── google/api/ # Google API 注解(HTTP mapping)
├── google/protobuf/ # Protobuf 标准类型
├── openapi/v3/ # OpenAPI 注解
└── validate/ # 校验规则注解

kratos-layout 采用的是 整洁架构(Clean Architecture),也被称为洋葱架构或六边形架构。其核心思想是依赖关系只能由外向内,内层不能依赖外层。关于整洁架构,之前在分析 go-clean-template 项目时也完整介绍过。

1
2
3
请求 ──► Server(HTTP/gRPC)──► Service ──► Biz(Usecase)──► Data(Repo)
│ │ │ │
传输层 接口适配层 业务逻辑层 数据访问层
层级 目录 职责 DDD 对应
API api/ Proto 定义 API,自动生成 HTTP/gRPC 代码 -
Server internal/server/ 创建 HTTP/gRPC Server,配置中间件,注册路由 -
Service internal/service/ 实现 Proto 生成的接口,处理 DTO 转换,委托给 Biz Application 层
Biz internal/biz/ 业务逻辑与领域模型,定义 Repo 接口(依赖倒置) Domain 层
Data internal/data/ 数据访问,实现 Biz 层定义的 Repo 接口 Infrastructure 层
Conf internal/conf/ 配置结构定义(Proto 生成) -

这里所采用的关键设计是:biz 层定义 Repo 接口,data 层实现它。这是典型的依赖倒置原则(DIP),使得业务逻辑完全不依赖具体的数据访问实现。

API 层

业务 Proto 定义

首先我们来看下 API 层的实现。api/helloworld/v1/greeter.proto 定义了 Greeter 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";

package helloworld.v1;

import "google/api/annotations.proto";

option go_package = "github.com/go-kratos/kratos-layout/api/helloworld/v1;v1";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/helloworld/{name}"
};
}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}
  • google.api.http 注解:将 SayHello 方法映射为 GET /helloworld/{name},Kratos 会据此自动生成 HTTP 路由代码

  • google.api.http 是 gRPC 官方标准的 HTTP 映射注解,用来直接给 gRPC 方法定义 RESTful API 路由、HTTP 方法、路径参数、查询参数、请求体,让 gRPC 服务自动对外提供 HTTP/JSON 接口。它的核心作用是:实现 一套代码,双协议服务

  • 版本化包路径 api/helloworld/v1/

    • 遵循 Go 的导入兼容性规则——如果旧包和新包使用相同导入路径,它们必须兼容
    • 将 API 放在 /v1/ 目录下,包名为 helloworld.v1,将来做不兼容变更时新建 v2/ 目录(包名 helloworld.v2),两版 API 可以共存,旧客户端无需立即迁移
  • "github.com/go-kratos/kratos-layout/api/helloworld/v1;v1"; 中的分号把 go_package 分成两部分

    • 分号前:Go 的导入路径,即 import “github.com/go-kratos/kratos-layout/api/helloworld/v1” 用的路径
    • 分号后:Go 源文件里的 package v1 声明
    • 如果不加分号,protoc 会取路径最后一段作为包名,这里刚好也是 v1,所以效果一样
    • 但分号让你可以让包名和路径最后一段不同,比如:
1
2
// 路径是 v1,但希望包名叫 helloworld(更语义化)
option go_package = "github.com/foo/api/helloworld/v1;helloworld";

错误枚举

api/helloworld/v1/error_reason.proto 定义了业务错误码:

1
2
3
4
5
6
7
8
9
10
syntax = "proto3";

package helloworld.v1;

option go_package = "github.com/go-kratos/kratos-layout/api/helloworld/v1;v1";

enum ErrorReason {
GREETER_UNSPECIFIED = 0;
USER_NOT_FOUND = 1;
}

生成代码

执行 make api 命令时,会自动根据 Proto 文件自动生成 Go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
API_PROTO_FILES=$(shell find api -name *.proto)

.PHONY: api
# generate api proto
api:
protoc --proto_path=./api \
--proto_path=./third_party \
--go_out=paths=source_relative:./api \
--go-http_out=paths=source_relative:./api \
--go-grpc_out=paths=source_relative:./api \
--openapi_out=fq_schema_naming=true,default_response=false:. \
$(API_PROTO_FILES)

  • protoc 是 Protocol Buffers 的官方编译器,是整个代码生成的核心。它的核心职责非常纯粹:解析 .proto 文件。

  • 它负责读取你编写的 .proto 接口定义文件,检查语法是否正确

  • 理解文件中定义的 message(数据结构)和 service(RPC 服务)

然而,protoc 本身并不负责直接生成特定语言的代码(比如 Go、Java、Python 等代码都不是它亲自写的),它更像是一个流水线的前端解析器或总指挥官,而插件才是真正的 方言翻译官

  • 当你在命令行中传入 --<name>_out 参数时,protoc 就会自动在系统的环境变量(PATH)中寻找名为 protoc-gen-<name> 的可执行文件作为插件
  • 如果你写 --go_out=.,protoc 就会自动去寻找 protoc-gen-go 插件
  • 如果你写 --go-grpc_out=.,protoc 就会自动去寻找 protoc-gen-go-grpc 插件

有了以上的背景知识,我们就能理解上面命令中的关键参数:

  • --proto_path:指定 .proto 文件的搜索路径,./api 是项目自身的 Proto,./third_party 是第三方 Proto 依赖(如 google/api/annotations.proto
  • --go_out:指定 protoc-gen-go 插件的输出目录
  • --go-http_out:指定 protoc-gen-go-http 插件的输出目录
  • --go-grpc_out:指定 protoc-gen-go-grpc 插件的输出目录
  • --openapi_out:指定 protoc-gen-openapi 插件的输出目录
  • paths=source_relative:生成的 .go 文件与 .proto 文件放在同一目录,而不是按 Go 包路径创建子目录

protoc-gen-go 是 Go 官方的 Protobuf 代码生成插件。它为每个 .proto 文件生成一个 .pb.go 文件。对应 greeter.proto 生成的 greeter.pb.go,包含

  • 消息类型:如 HelloRequestHelloReply,带有完整的序列化/反序列化、反射支持
  • 枚举类型:如 ErrorReason
  • 文件描述符:用于运行时反射

protoc-gen-go-grpc 是 gRPC 官方的 Go 代码生成插件。它为 Proto 中的 service 定义生成 _grpc.pb.go。对应 greeter.proto 生成的 greeter_grpc.pb.go,包含:

  • GreeterClient 接口:客户端调用接口
  • greeterClient 实现:客户端实现,通过 grpc.ClientConn 发起 RPC 调用
  • GreeterServer 接口:服务端实现接口(包含 mustEmbedUnimplementedGreeterServer() 保证前向兼容)
  • UnimplementedGreeterServer:空实现基类,未实现的方法返回 Unimplemented 错误
  • RegisterGreeterServer:将服务实现注册到 grpc.ServiceRegistrar
  • Greeter_ServiceDesc:服务描述符,定义方法名、Handler 等元信息

protoc-gen-go-http 则是 Kratos 特有的代码生成插件。它读取 Proto 中的 google.api.http 注解,生成 _http.pb.go 文件,将 gRPC 方法映射为 HTTP 路由,对应 greeter.proto 生成的 greeter_http.pb.go,包含:

  • GreeterHTTPServer 接口:HTTP 服务端接口,方法签名与 GreeterServer 相同
  • RegisterGreeterHTTPServer:将 HTTP 服务实现注册到 Kratos http.Server
  • HTTP Handler:将 HTTP 请求参数绑定到 Proto 消息,穿过中间件链,调用业务方法
  • GreeterHTTPClient 接口:HTTP 客户端接口
  • GreeterHTTPClientImpl:HTTP 客户端实现,通过 Kratos http.Client 发起 HTTP 调用

protoc-gen-openapi 是 Google 的 OpenAPI 文档生成插件。它读取 Proto 中的 google.api.http 注解,生成 openapi.yaml 文件,可直接导入 Swagger UI 查看 API 文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Generated with protoc-gen-openapi
# https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi

openapi: 3.0.3
info:
title: Greeter API
description: The greeting service definition.
version: 0.0.1
paths:
/helloworld/{name}:
get:
tags:
- Greeter
- subgroup
......

总结一下所使用的工具和生成的代码:

工具 来源 生成文件 作用
protoc Google - Proto 编译器,解析 .proto 文件并调用插件
protoc-gen-go Go 官方 .pb.go 生成 Protobuf 消息类型和枚举
protoc-gen-go-grpc gRPC 官方 _grpc.pb.go 生成 gRPC 服务端/客户端代码
protoc-gen-go-http Kratos _http.pb.go 根据 google.api.http 注解生成 HTTP 路由代码
protoc-gen-openapi Google openapi.yaml 生成 OpenAPI/Swagger 文档

配置层

Proto 定义配置结构

kratos-layout 的配置结构不是手动定义 Go struct,而是通过 Proto 文件定义,再生成 Go 代码。配置 proto 文件 internal/conf/conf.proto 的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
syntax = "proto3";
package kratos.api;

option go_package = "github.com/go-kratos/kratos-layout/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;
}

使用 Proto 定义配置结构的好处:

  1. 类型安全:字段类型明确,例如 google.protobuf.Duration 可以精确表达时间类型
  2. 文档化:Proto 文件本身就是配置的文档
  3. 跨语言兼容:如果需要,其他语言的服务也可以读取同一份配置定义
  4. 版本管理:与 API 定义一样,配置结构也可以做版本管理

生成代码

执行 make config 命令,会使用 protoc-gen-gointernal/conf/conf.proto 生成 conf.pb.go(不涉及 gRPC 和 HTTP 插件)

1
2
3
4
5
6
7
8
9
INTERNAL_PROTO_FILES=$(shell find internal -name *.proto)

.PHONY: config
# generate internal proto
config:
protoc --proto_path=./internal \
--proto_path=./third_party \
--go_out=paths=source_relative:./internal \
$(INTERNAL_PROTO_FILES)

通过以上方法,就得到了 Go 代码形式的配置结构体。

配置文件

configs/config.yaml 是实际运行的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
http:
addr: 0.0.0.0:8000
timeout: 1s
grpc:
addr: 0.0.0.0:9000
timeout: 1s
data:
database:
driver: mysql
source: root:root@tcp(127.0.0.1:3306)/test?parseTime=True&loc=Local
redis:
addr: 127.0.0.1:6379
read_timeout: 0.2s
write_timeout: 0.2s

业务逻辑

接下来将详细分析业务逻辑是如何实现的。如下整体展示了业务逻辑的组织方式:

1
2
3
4
service ──依赖──► biz(接口 + 用例)

│ 实现
data(Repo 实现)

Service 层

Service 层是 DDD 中的 Application 层,它实现了 Proto 生成的接口,负责 DTO(数据传输对象)和 DO(领域对象)之间的转换。Service 层应该保持轻量,只做参数转换和编排,不包含业务逻辑。

如下是 internal/service/greeter.go 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package service

import (
"context"

v1 "github.com/go-kratos/kratos-layout/api/helloworld/v1"
"github.com/go-kratos/kratos-layout/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
}

关键设计点:

  • 嵌入 UnimplementedGreeterServer:这是 gRPC 的前向兼容设计,当 Proto 文件新增方法时,未实现的方法会返回 Unimplemented 错误,而不是编译失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 接口要求实现两个方法
type GreeterServer interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
mustEmbedUnimplementedGreeterServer() // 关键:这个方法
}

// 空实现基类
type UnimplementedGreeterServer struct{}

func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}

func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}

// mustEmbedUnimplementedGreeterServer() 强制你嵌入 UnimplementedGreeterServer。因为 Go 接口满足是编译期检查的,你的 struct 不嵌入它就编译不过,也就无法注册服务

// 嵌入后,新增方法不会编译失败。假设 Proto 新增了 rpc Goodbye,重新生成后 GreeterServer 接口多了 Goodbye 方法。如果你没实现它,嵌入的 UnimplementedGreeterServer 提供了默认实现,返回 Unimplemented 错误,编译通过,运行时客户端会收到明确的错误码,而不是服务端 panic
  • 依赖 GreeterUsecase:Service 不直接处理业务逻辑,而是委托给 Biz 层的 Usecase
  • DTO 转换HelloRequest(Proto DTO)→ biz.Greeter(领域对象)→ HelloReply(Proto DTO)

Biz 层

Biz 层是 DDD 中的 Domain 层,是整个项目的核心。它包含业务逻辑、领域模型,以及定义 Repo 接口。来看 internal/biz/greeter.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package biz

import (
"context"

v1 "github.com/go-kratos/kratos-layout/api/helloworld/v1"

"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/log"
)

var (
ErrUserNotFound = errors.NotFound(v1.ErrorReason_USER_NOT_FOUND.String(), "user not found")
)

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
}

func NewGreeterUsecase(repo GreeterRepo) *GreeterUsecase {
return &GreeterUsecase{repo: repo}
}

func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
log.Infof("CreateGreeter: %v", g.Hello)
return uc.repo.Save(ctx, g)
}

这是整个模板中最重要的一层,核心设计点:

  • 依赖倒置(DIP)GreeterRepo 接口定义在 biz 层,而不是 data 层。biz 不依赖 data,而是 data 依赖 biz 来实现接口。这样业务逻辑完全不关心数据来源(数据库、缓存、外部服务等)
  • Usecase 模式GreeterUsecase 封装业务用例,一个 Usecase 可以组合多个 Repo,编排复杂的业务流程
  • 领域模型Greeter 是领域对象,与 Proto 生成的 HelloRequest/HelloReply 分离,领域对象专注于业务语义
  • 错误定义:使用 errors.NotFound() 配合 Proto 生成的 ErrorReason_USER_NOT_FOUND 枚举,形成统一的错误体系

Data 层

Data 层实现了 Biz 层定义的 Repo 接口,负责与具体的数据源(数据库、缓存、外部 API)交互。如下是 internal/data/greeter.go 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package data

import (
"context"

"github.com/go-kratos/kratos-layout/internal/biz"

"github.com/go-kratos/kratos/v2/log"
)

type greeterRepo struct {
data *Data
log *log.Helper
}

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
}

func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
return g, nil
}

func (r *greeterRepo) FindByID(context.Context, int64) (*biz.Greeter, error) {
return nil, nil
}

func (r *greeterRepo) ListByHello(context.Context, string) ([]*biz.Greeter, error) {
return nil, nil
}

func (r *greeterRepo) ListAll(context.Context) ([]*biz.Greeter, error) {
return nil, nil
}

如下则是 internal/data/data.go 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package data

import (
"github.com/go-kratos/kratos-layout/internal/conf"

"github.com/go-kratos/kratos/v2/log"
"github.com/google/wire"
)

var ProviderSet = wire.NewSet(NewData, NewGreeterRepo)

type Data struct {
// TODO wrapped database client
}

func NewData(c *conf.Data) (*Data, func(), error) {
cleanup := func() {
log.Info("closing the data resources")
}
return &Data{}, cleanup, nil
}

关键设计点:

  • 实现 Biz 接口NewGreeterRepo 返回的是 biz.GreeterRepo 接口类型,不是具体类型。这保证了依赖倒置
  • Data 结构体:封装数据源连接(数据库客户端、Redis 客户端等),当前是 TODO 占位
  • cleanup 函数NewData 返回一个清理函数,用于关闭数据库连接等资源,在应用停止时由 Wire 自动调用
  • log.Helper:使用 Kratos 的日志 Helper,支持结构化日志输出

框架代码

在分析了核心业务逻辑的实现之后,我们再来看这个 App 的框架代码,包括 Server 的启动、依赖注入、程序入口等逻辑。

Server 层

Server 层负责创建和配置 HTTP/gRPC Server,注册中间件和服务路由。internal/server/http.go 实现了 HTTP Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package server

import (
v1 "github.com/go-kratos/kratos-layout/api/helloworld/v1"
"github.com/go-kratos/kratos-layout/internal/conf"
"github.com/go-kratos/kratos-layout/internal/service"

"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/transport/http"
)

func NewHTTPServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *http.Server {
var opts = []http.ServerOption{
http.Middleware(
recovery.Recovery(),
),
}
if c.Http.Network != "" {
opts = append(opts, http.Network(c.Http.Network))
}
if c.Http.Addr != "" {
opts = append(opts, http.Address(c.Http.Addr))
}
if c.Http.Timeout != nil {
opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))
}
srv := http.NewServer(opts...)
v1.RegisterGreeterHTTPServer(srv, greeter)
return srv
}

internal/server/grpc.go 则实现了 grpc Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package server

import (
v1 "github.com/go-kratos/kratos-layout/api/helloworld/v1"
"github.com/go-kratos/kratos-layout/internal/conf"
"github.com/go-kratos/kratos-layout/internal/service"

"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/transport/grpc"
)

func NewGRPCServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *grpc.Server {
var opts = []grpc.ServerOption{
grpc.Middleware(
recovery.Recovery(),
),
}
if c.Grpc.Network != "" {
opts = append(opts, grpc.Network(c.Grpc.Network))
}
if c.Grpc.Addr != "" {
opts = append(opts, grpc.Address(c.Grpc.Addr))
}
if c.Grpc.Timeout != nil {
opts = append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration()))
}
srv := grpc.NewServer(opts...)
v1.RegisterGreeterServer(srv, greeter)
return srv
}

关键设计点:

  1. 配置驱动:Server 的网络、地址、超时等参数全部从 conf.Server 读取,而非硬编码
  2. 中间件注册:默认注册了 recovery.Recovery(),开发者可以按需添加 tracingloggingvalidate 等中间件
  3. 服务注册:通过 v1.RegisterGreeterHTTPServer()v1.RegisterGreeterServer() 将 Service 实现注册到对应的 Server
  4. 依赖注入greeter *service.GreeterService 通过 Wire 注入,不需要手动创建

Wire 依赖注入

kratos-layout 使用 Google Wire 进行编译时依赖注入,避免运行时反射带来的性能开销。

首先来看 App 的构建,参见代码 cmd/server/wire.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//go:build wireinject

package main

import (
"github.com/go-kratos/kratos-layout/internal/biz"
"github.com/go-kratos/kratos-layout/internal/conf"
"github.com/go-kratos/kratos-layout/internal/data"
"github.com/go-kratos/kratos-layout/internal/server"
"github.com/go-kratos/kratos-layout/internal/service"

"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/wire"
)

func wireApp(*conf.Server, *conf.Data, log.Logger) (*kratos.App, func(), error) {
panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}
  • 每个层通过 ProviderSet 声明该层提供的依赖:
  • 注意 //go:build wireinject 构建标签确保此文件不会编入最终二进制
  • 如果我们要看最终的 App 构建逻辑,可以查看生成的 cmd/server/wire_gen.go 文件。
  • wire 命令读取 wire.go 里的 wire.Build(...) 声明,分析所有 Provider 的参数和返回类型,自动推导出完整的依赖图,然后生成 wire_gen.go 里的组装代码
  • 如果要重新生成依赖,可以通过使用项目根目录的 Makefile,执行 make generate 命令

我们来简单看下各层的 ProviderSet

1
2
3
4
5
6
7
8
9
10
11
// internal/server/server.go
var ProviderSet = wire.NewSet(NewGRPCServer, NewHTTPServer)

// internal/data/data.go
var ProviderSet = wire.NewSet(NewData, NewGreeterRepo)

// internal/biz/biz.go
var ProviderSet = wire.NewSet(NewGreeterUsecase)

// internal/service/service.go
var ProviderSet = wire.NewSet(NewGreeterService)

主入口

cmd/server/main.go 是整个应用的启动入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package main

import (
"flag"
"os"

"github.com/go-kratos/kratos-layout/internal/conf"

"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/config"
"github.com/go-kratos/kratos/v2/config/file"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/tracing"
"github.com/go-kratos/kratos/v2/transport/grpc"
"github.com/go-kratos/kratos/v2/transport/http"

_ "go.uber.org/automaxprocs"
)

var (
Name string
Version string
flagconf string
id, _ = os.Hostname()
)

func init() {
flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml")
}

func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server) *kratos.App {
return kratos.New(
kratos.ID(id),
kratos.Name(Name),
kratos.Version(Version),
kratos.Metadata(map[string]string{}),
kratos.Logger(logger),
kratos.Server(gs, hs),
)
}

func main() {
flag.Parse()
logger := log.With(log.NewStdLogger(os.Stdout),
"ts", log.DefaultTimestamp,
"caller", log.DefaultCaller,
"service.id", id,
"service.name", Name,
"service.version", Version,
"trace.id", tracing.TraceID(),
"span.id", tracing.SpanID(),
)
c := config.New(
config.WithSource(file.NewSource(flagconf)),
)
defer c.Close()

if err := c.Load(); err != nil {
panic(err)
}

var bc conf.Bootstrap
if err := c.Scan(&bc); err != nil {
panic(err)
}

app, cleanup, err := wireApp(bc.Server, bc.Data, logger)
if err != nil {
panic(err)
}
defer cleanup()

if err := app.Run(); err != nil {
panic(err)
}
}

启动流程拆解:

  1. flagconf 配置路径:默认为 ../../configs(相对于 cmd/server/ 的路径),可通过 -conf 参数覆盖
  2. Logger 初始化:使用 log.With() 为日志添加固定字段(时间戳、调用者、服务信息、TraceID 等)
  3. 配置加载config.New() + config.WithSource() + c.Load() + c.Scan() 四步加载并解析配置
  4. Wire 组装wireApp(bc.Server, bc.Data, logger) 根据配置和日志创建完整的 App
  5. 启动运行app.Run() 启动所有 Server 并阻塞等待信号

另外注意:

  • _ "go.uber.org/automaxprocs":自动调整 GOMAXPROCS,在容器环境(Docker/K8s)中非常重要,因为默认 Go 会读取宿主机的 CPU 核数而非容器的 CPU 限制
  • Name/Version 通过 -ldflags 注入:编译时通过 go build -ldflags "-X main.Version=x.y.z" 注入版本信息

工程化

最后简单介绍下该项目提供的一些工程化特性。

Makefile

首先 Makefile 封装了常用的代码生成和构建命令:

命令 作用
make init 安装 protoc 生成工具链(protoc-gen-go、protoc-gen-go-grpc、protoc-gen-go-http、wire 等)
make config 根据 internal/conf/conf.proto 生成配置结构体
make api 根据 api/ 下的 Proto 文件生成 API 代码(pb.go、gRPC、HTTP、OpenAPI)
make build 编译项目,通过 -ldflags 注入版本号
make generate 执行 go generate(重新生成 Wire 代码)并整理依赖
make all 依次执行 apiconfiggenerate

典型的工作流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 首次安装工具链
make init

# 2. 修改 Proto 文件后重新生成
make api

# 3. 修改 conf.proto 后重新生成配置
make config

# 4. 添加新依赖后重新生成 Wire 代码
make generate

# 5. 编译运行
make build
./bin/server -conf ./configs

Dockerfile 容器化部署

Dockerfile 提供了容器化部署的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FROM golang:1.19 AS builder

COPY . /src
WORKDIR /src

RUN GOPROXY=https://goproxy.cn make build

FROM debian:stable-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
netbase \
&& rm -rf /var/lib/apt/lists/ \
&& apt-get autoremove -y && apt-get autoclean -y

COPY --from=builder /src/bin /app

WORKDIR /app

EXPOSE 8000
EXPOSE 9000
VOLUME /data/conf

CMD ["./server", "-conf", "/data/conf"]
  • 多阶段构建:builder 阶段编译,最终镜像只包含二进制和运行时依赖,镜像体积小
  • VOLUME /data/conf:配置文件通过 Volume 挂载,便于在不同环境使用不同配置
  • EXPOSE 8000 9000:分别暴露 HTTP 和 gRPC 端口

小结

这篇文章我们详细介绍了 kratos-layout 模板项目的实现原理,它是 kratos 所使用的 Go 微服务项目官方模板,这个项目模版使用了 整洁架构Wire 依赖注入 等 Go 开发的最佳实践。