上篇文章我们已经快速使用了 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 分成两部分
1 2
| 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
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,包含
- 消息类型:如
HelloRequest、HelloReply,带有完整的序列化/反序列化、反射支持
- 枚举类型:如
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
|
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 定义配置结构的好处:
- 类型安全:字段类型明确,例如
google.protobuf.Duration 可以精确表达时间类型
- 文档化:Proto 文件本身就是配置的文档
- 跨语言兼容:如果需要,其他语言的服务也可以读取同一份配置定义
- 版本管理:与 API 定义一样,配置结构也可以做版本管理
生成代码
执行 make config 命令,会使用 protoc-gen-go 将 internal/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
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() {}
|
- 依赖
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 { }
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 }
|
关键设计点:
- 配置驱动:Server 的网络、地址、超时等参数全部从
conf.Server 读取,而非硬编码
- 中间件注册:默认注册了
recovery.Recovery(),开发者可以按需添加 tracing、logging、validate 等中间件
- 服务注册:通过
v1.RegisterGreeterHTTPServer() 和 v1.RegisterGreeterServer() 将 Service 实现注册到对应的 Server
- 依赖注入:
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
|
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
| var ProviderSet = wire.NewSet(NewGRPCServer, NewHTTPServer)
var ProviderSet = wire.NewSet(NewData, NewGreeterRepo)
var ProviderSet = wire.NewSet(NewGreeterUsecase)
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) } }
|
启动流程拆解:
flagconf 配置路径:默认为 ../../configs(相对于 cmd/server/ 的路径),可通过 -conf 参数覆盖
- Logger 初始化:使用
log.With() 为日志添加固定字段(时间戳、调用者、服务信息、TraceID 等)
- 配置加载:
config.New() + config.WithSource() + c.Load() + c.Scan() 四步加载并解析配置
- Wire 组装:
wireApp(bc.Server, bc.Data, logger) 根据配置和日志创建完整的 App
- 启动运行:
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 |
依次执行 api → config → generate |
典型的工作流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| make init
make api
make config
make generate
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 开发的最佳实践。