这篇文章我们继续分析 kratos CLI 命令行工具的实现。我们将首先介绍 kratos CLI 工具的用法,之后再从源码层面分析其实现原理。
kratos CLI 概览
kratos CLI 是基于 cobra 框架构建的命令行工具,用于项目脚手架生成、Proto 文件管理和代码生成,用于简化 kratos 微服务的开发流程。
kratos CLI 共提供 5 个顶级命令:
1 | kratos (root) |
| 命令 | 简述 | 示例 |
|---|---|---|
kratos new |
创建服务模板项目 | kratos new helloworld |
kratos proto |
Proto 文件生成(含 3 个子命令) | kratos proto add user/v1/user.proto |
kratos upgrade |
升级 kratos 相关工具 | kratos upgrade |
kratos changelog |
查看 kratos 版本变更日志 | kratos changelog dev |
kratos run |
运行项目 | kratos run |
kratos new
kratos new 用于基于远程模板仓库创建新的微服务项目,基本用法如下:
1 | kratos new <项目名称> |
支持的 Flags:
| Flag | 缩写 | 默认值 | 说明 |
|---|---|---|---|
--repo-url |
-r |
https://github.com/go-kratos/kratos-layout.git |
模板仓库地址 |
--branch |
-b |
空(默认分支) | 仓库分支 |
--timeout |
-t |
60s |
超时时间 |
--nomod |
- | false |
保留现有 go.mod(用于在单体仓库中添加子服务) |
环境变量:
| 变量名 | 说明 |
|---|---|
KRATOS_LAYOUT_REPO |
自定义模板仓库地址,优先于默认值 |
该命令默认使用我们上篇文章介绍的 kratos-layout 模版来创建项目。这里特别注意其两种创建模式:
- 普通模式(默认):创建独立项目,生成新的
go.mod,模板中cmd/server会被重命名为cmd/<项目名> --nomod模式:在现有项目中添加子服务,共用同一个go.mod,模板中的api路径会根据子服务的相对位置进行调整
kratos proto
kratos proto 用于管理 Proto 文件及生成代码,包含三个子命令。
kratos proto add
kratos proto add 用于快速生成一个标准 CRUD 风格的 Proto API 文件。基本用法:
1 | kratos proto add <路径/文件名.proto> |
例如:
1 | # kratos proto add api/helloworld/v1/demo.proto |
kratos proto client
kratos proto client 用于调用 protoc 及其插件,从 .proto 文件编译生成全部相关代码(Protobuf 消息类型、gRPC 服务端/客户端、HTTP 服务端/客户端、错误码、OpenAPI 文档等)。虽然命令名为 client,但实际生成的代码并不局限于客户端,而是涵盖了 proto 编译的全部产物。
1 | kratos proto client <proto文件或目录> |
支持的 Flags:
| Flag | 缩写 | 默认值 | 说明 |
|---|---|---|---|
--proto_path |
-p |
./third_party |
Proto 搜索路径 |
环境变量:
| 变量名 | 说明 |
|---|---|
KRATOS_PROTO_PATH |
自定义 Proto 搜索路径 |
例如:
1 | # kratos proto client api/helloworld/v1/greeter.proto |
该命令会自动调用以下 protoc 插件生成代码(注意 http 代码只会在 proto 文件中声明了 http 时才会生成):
| 插件 | 生成内容 |
|---|---|
protoc-gen-go |
Go Protobuf 消息类型(.pb.go) |
protoc-gen-go-grpc |
gRPC 客户端/服务端代码(_grpc.pb.go) |
protoc-gen-go-http |
Kratos HTTP 客户端代码(_http.pb.go) |
protoc-gen-go-errors |
Kratos 错误代码(_errors.pb.go) |
protoc-gen-openapi |
OpenAPI/Swagger 文档(.openapi.yaml) |
1 | # ls -l api/helloworld/v1/greeter* |
如果检测到 proto 文件中 import "validate/validate.proto",还会自动添加 --validate_out 插件。如果缺少必要的 protoc 插件,命令会自动执行 kratos upgrade 进行安装。
kratos proto server
kratos proto server 用于根据 .proto 文件中的 service 定义,来生成 Go 服务端实现骨架代码。基本用法如下:
1 | kratos proto server <proto文件> --target-dir=<目标目录> |
支持的 Flags:
| Flag | 缩写 | 默认值 | 说明 |
|---|---|---|---|
--target-dir |
-t |
internal/service |
生成文件的目标目录 |
例如:
1 | # kratos proto server api/helloworld/v1/greeter.proto |
kratos upgrade
kratos upgrade 用于一键安装/升级 kratos 开发所需的所有工具。该命令会执行 go install 安装以下 6 个工具:
github.com/go-kratos/kratos/cmd/kratos/v2@latest— kratos CLI 本身google.golang.org/protobuf/cmd/protoc-gen-go@latest— Go protobuf 插件github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latest— 错误代码生成插件google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest— gRPC 插件github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest— HTTP 代码生成插件github.com/google/gnostic/cmd/protoc-gen-openapi@latest— OpenAPI 插件
kratos changelog
kratos changelog 用于查看 kratos 的版本发布信息或开发中的提交记录。基本用法如下:
1 | kratos changelog [dev | 版本号] |
- 当使用
dev参数时,命令会获取自上次发布以来的所有 commit,并按前缀分类输出 Markdown 格式的变更日志
支持的 Flags:
| Flag | 缩写 | 默认值 | 说明 |
|---|---|---|---|
--repo-url |
-r |
https://github.com/go-kratos/kratos.git |
GitHub 仓库地址 |
环境变量:
| 变量名 | 说明 |
|---|---|
KRATOS_REPO |
自定义 GitHub 仓库地址 |
GITHUB_TOKEN |
GitHub API Token(可选,用于提高 API 请求限制) |
kratos run
kratos run 用于快速运行 kratos 项目,自动查找 cmd/ 目录下的服务入口。
- 如果需要传递参数给要运行的程序本身,用
--分隔 - 如果
cmd/下只有一个子目录,则自动选择运行;如果有多个子目录,会弹出交互式选择界面
支持的 Flags:
| Flag | 缩写 | 默认值 | 说明 |
|---|---|---|---|
--work |
-w |
空 | 目标工作目录 |
1 | # kratos run |
整体架构
目录结构
kratos CLI 的源码位于 cmd/kratos/ 目录下,整体目录结构如下所示:
1 | cmd/kratos/ |
分层注册
入口文件 cmd/kratos/main.go 使用 cobra 框架定义根命令并注册子命令:
1 | var rootCmd = &cobra.Command{ |
命令注册采用分层结构:
- 根级别(
main.go的init()):通过rootCmd.AddCommand(...)注册 5 个顶级命令 - 嵌套命令(如
proto):在proto.go的init()中注册子命令
1 | // cmd/kratos/internal/proto/proto.go |
每个命令的 Flags 在各自包的 init() 函数中注册。例如 kratos new 的 Flags 定义在 project.go 中:
1 | func init() { |
接下来我们详细分析每个命令的实现。
kratos new 实现
kratos new 是 kratos CLI 中最复杂的命令,核心逻辑分为两个分支:独立项目创建(Project.New())和单体仓库子服务添加(Project.Add())。该命令的整体实现流程如下:
1 | 用户输入 kratos new helloworld |
独立项目创建
Project.New() 定义在 cmd/kratos/internal/project/new.go,流程如下:
- 检查目标目录:如果目录已存在,询问用户是否覆盖
- 创建 Repo 对象:从模板仓库 URL 创建
base.Repo实例 - 复制模板:调用
repo.CopyTo()克隆模板仓库到缓存,然后复制到目标目录,同时替换 Go module 名称 - 重命名 cmd/server:将
cmd/server重命名为cmd/<项目名> - 显示文件树:调用
base.Tree()展示所有创建的文件 - 输出后续步骤:打印
cd、go generate、go build、运行等命令
单体仓库子服务添加
Project.Add() 定义在 cmd/kratos/internal/project/add.go,与 New() 的主要区别是:
- 忽略更多文件(
api、go.mod、go.sum、third_party、README.md等) - 使用
CopyToV2()替代CopyTo(),支持额外的字符串替换 - 子服务不会拥有独立的
api/,,让子服务指向根项目共享的api/目录
1 | pkgPath = fmt.Sprintf("%s/%s", mod, pkgPath) |
CopyToV2() 相比 CopyTo() 多了一个 replaces 参数,用于额外的字符串替换对。这里将 {子服务路径}/api 替换为 api,确保 import 路径正确。
这种 Add 方式执行的操作还是有点复杂,以下用一个具体场景逐步拆解假设目录结构如下:
1 | /home/user/my-monorepo/ # projectRoot |
用户在 /home/user/my-monorepo/services/ 下执行:
1 | kratos new user --nomod |
第 1 步:获取初始值
1 | wd = os.Getwd() // "/home/user/my-monorepo/services" |
第 2 步:processProjectParams("user", wd)
1 | // "user" 是相对路径,先转为绝对路径 |
第 3 步:projectRoot = getgomodProjectRoot(workingDir)
从 /home/user/my-monorepo/services 向上查找 go.mod:
1 | /home/user/my-monorepo/services → 无 go.mod → 继续 |
1 | projectRoot = "/home/user/my-monorepo" |
第 4 步:packagePath — 新服务相对于项目根的路径
1 | packagePath = filepath.Rel(projectRoot, filepath.Join(workingDir, projectName)) |
第 5 步:mod — 从 go.mod 读取 module 名
1 | mod = base.ModulePath("/home/user/my-monorepo/go.mod") |
第 6 步:p.Path — 服务的相对路径(用于 api 路径替换)
1 | p.Path = filepath.Join( |
TrimPrefix 把 workingDir 中 projectRoot+"/" 的部分去掉,得到当前工作目录相对于项目根的相对路径,再拼接项目名。
第 7 步:p.Add() 中的 pkgPath 变换
1 | pkgPath = fmt.Sprintf("%s/%s", mod, pkgPath) |
这是新服务在 Go module 中的完整 import 路径,将作为 CopyToV2 中替换模板 module 名的目标值。
第 8 步:CopyToV2 的替换对
1 | replaces = []string{ |
经过 copyDir → copyFile,所有文件中的字符串替换为:
| 替换 | 说明 |
|---|---|
github.com/go-kratos/kratos-layout → github.com/myorg/my-monorepo/services/user |
module 路径替换 |
services/user/api → api |
api 路径回正 |
第二个替换的原因:模板中的 import 路径是 github.com/go-kratos/kratos-layout/api,经过第一步替换后变成 github.com/myorg/my-monorepo/services/user/api。但在单体仓库中,api 目录被忽略了(在 repoAddIgnores 中),子服务不会拥有独立的 api/,所以需要将 import 中的 {子服务路径}/api 替换回 api,让子服务指向根项目共享的 api/ 目录。
模板仓库缓存机制
kratos CLI 的项目创建采用远程仓库克隆 + 本地缓存的方式,而非本地内嵌模板。核心实现在 cmd/kratos/internal/base/repo.go 中。
1 | type Repo struct { |
NewRepo()构造函数计算缓存路径repoDir()函数从 URL 中提取缓存子目录Path()方法返回完整的缓存路径
例如 https://github.com/go-kratos/kratos-layout.git 的缓存路径为:
1 | # ls ~/.kratos/repo/github.com/go-kratos/kratos-layout@main |
Clone() 用于从远程仓库 clone 或者 pull:
1 | func (r *Repo) Clone(ctx context.Context) error { |
复制与模块名替换
CopyTo() 方法完成从缓存到目标目录的复制,同时替换 Go module 名称:
1 | func (r *Repo) CopyTo(ctx context.Context, to string, modPath string, ignores []string) error { |
copyDir() 函数(定义在 base/path.go)递归复制目录,对所有文件执行字符串替换(:
1 | func copyDir(src, dst string, replaces, ignores []string) error { |
copyFile()在复制文件时执行全量字符串替换replaces是一个扁平的[old1, new1, old2, new2, ...]序列。奇数索引为旧值,偶数索引为新值- 正是通过这种方式简单的方式——将模板中的原始 module 名(如
github.com/go-kratos/kratos-layout)替换为用户的项目名(如github.com/myorg/helloworld),影响所有文件
kratos proto 实现
kratos proto add
kratos proto add 的实现在 cmd/kratos/internal/proto/add/ 目录下。其中的一些关键函数:
add.go的run()函数解析用户输入的路径,提取出各组成部分,包括文件名、包名等goPackage()函数生成option go_package的值- 它读取当前项目的
go.mod文件获取 module 名称,然后拼接为{module}/{path};{lastSegment}的格式 - 例如
go.mod中 module 为github.com/myorg/myproject,路径为user/v1,则生成github.com/myorg/myproject/user/v1;v1。
- 它读取当前项目的
serviceName()函数将 proto 文件名转为大驼峰服务名- 例如
user.proto→User,hello_world.proto→HelloWorld
- 例如
template.go 定义了 proto 文件模板,使用 Go 标准库 text/template 渲染,模板生成 5 个标准 CRUD RPC 方法和 10 个空消息体(Request/Reply 对):
1 | const protoTemplate = ` |
最后 proto.go 的 Generate() 方法创建目录并将模板渲染后的内容写入新生成的 proto 文件中。
kratos proto client
kratos proto client 的实现在 cmd/kratos/internal/proto/client/client.go,它的核心是构建 protoc 命令行并作为其作为子进程执行。虽然命令名为 client,但它实际上会调用所有 protoc 插件,对 proto 文件进行编译并生成产物(包括服务端和客户端代码)。
该命令的总体实现流程如下:
1 | 用户输入 kratos proto client helloworld.proto |
generate() 函数是该命令的核心,它构建完整的 protoc 命令行:
1 | func generate(proto string, args []string) error { |
--proto_path层次:当前目录→用户 third_party→Kratos 模块路径→Kratos third_party,按优先级从高到低paths=source_relative:输出文件与 proto 文件保持相对路径一致,生成在同级目录base.KratosMod():返回 kratos 模块在 Go mod cache 中的绝对路径(如/root/go/pkg/mod/github.com/go-kratos/kratos/v2@v2.7.2),让 protoc 能找到 kratos 提供的 proto 定义
KratosMod() 定义在 base/mod.go,用于定位 kratos 模块在 Go mod cache 中的路径:
1 | func KratosMod() string { |
ModuleVersion() 通过 go mod graph 命令获取 kratos 的依赖版本:
1 | func ModuleVersion(path string) (string, error) { |
最终路径形如:/root/go/pkg/mod/github.com/go-kratos/kratos/v2@v2.7.2。该路径下包含 kratos 提供的 proto 文件:
1 | kratos/v2@v2.7.2/ |
这里解释再解释下 --proto_path 的作用。由于 proto 文件中可能引用其他 proto 文件,而 protoc 需要通过 --proto_path 知道去哪里找这些 import。具体来说有四层 --proto_path,每层解决不同的 import 需求:
--proto_path=.:找项目自身的 proto--proto_path=./third_party:找项目自定义的第三方 proto--proto_path={KratosMod}: 找 kratos 自身的 proto。例如业务代码如果使用 kratos 的 metadata 服务,就需要这样查找--proto_path={KratosMod}/third_party:找 kratos 内置的第三方 proto
kratos proto server
kratos proto server 的实现在 cmd/kratos/internal/proto/server/,与 proto client 不同,它不使用 protoc,而是直接使用 github.com/emicklei/proto 库解析 proto 文件,然后用 Go 模板生成服务端代码。
1 | 用户输入 kratos proto server api/helloworld/v1/greeter.proto |
getMethodType() 根据 StreamsRequest 和 StreamsReturns 两个布尔值判断 RPC 方法类型:
1 | func getMethodType(streamsRequest, streamsReturns bool) MethodType { |
template.go 定义了服务端代码模板,针对四种方法类型生成不同的方法签名:
1 | var serviceTemplate = ` |
模板的智能之处在于:
- 按需导入:只有在存在 unary 方法时才导入
context,只有在存在流式方法时才导入io - Empty 类型处理:如果请求或返回类型是
google.protobuf.Empty,则使用emptypb.Empty替代 - 流式方法骨架:自动生成
Recv/Send/SendAndClose的循环骨架代码
kratos upgrade 实现
kratos upgrade 的实现非常简洁,在 cmd/kratos/internal/upgrade/upgrade.go 中,直接调用 base.GoInstall() 安装 6 个工具
1 | func Run(_ *cobra.Command, _ []string) { |
GoInstall() 定义在 base/install.go,逐个执行 go install 命令。
kratos changelog 实现
kratos changelog 的实现在 cmd/kratos/internal/change/ 目录下,通过 GitHub REST API 获取版本信息。
1 | 用户输入 kratos changelog [dev | 版本号] |
GithubAPI结构体封装了与 GitHub API 的交互:GetReleaseInfo:获取 Release 信息GetCommitsInfo:获取 Commit 信息,dev 模式
ParseCommitsInfo()将 commit 按前缀分类并输出 Markdown 格式
kratos run 实现
kratos run 的实现在 cmd/kratos/internal/run/run.go,核心逻辑是自动查找 cmd/ 目录下的服务入口并执行 go run。
1 | 用户输入 kratos run [-- -conf ./configs] |
findCMD() 函数递归查找当前目录下的 cmd/ 子目录,返回的 map[string]string 中:
- key 是相对路径(如
cmd/kratos-demo),用于交互式选择 - value 是绝对路径(如
/home/user/project/cmd/kratos-demo),用于go run
第三方库
最后,简单总结下 kratos CLI 使用了以下直接依赖的第三方库:
| 库 | 用途 |
|---|---|
| github.com/spf13/cobra | 命令行框架,定义命令、子命令、Flags 等 |
| github.com/AlecAivazis/survey/v2 | 交互式终端输入,用于项目名输入、覆盖确认、目录选择等 |
| github.com/emicklei/proto | Proto 文件解析器,用于 kratos proto server 解析 service/RPC 定义 |
| github.com/fatih/color | 终端彩色输出,用于 CREATED、项目名等高亮显示 |
| golang.org/x/mod/modfile | 解析 go.mod 文件,提取 module 名称 |
| golang.org/x/text/cases | 大小写转换,用于蛇形命名转大驼峰(如 hello_world → HelloWorld) |
小结
在这篇文章中,我们详细分析了 kratos CLI 命令行工具的实现原理。我们重点解析了它如何将一个 GitHub 仓库作为模板,并通过模板渲染、Go 模块替换等操作,最终生成一个真实的用户项目。此外,我们还学习了如何根据 Proto 文件生成样本代码,包括调用 protoc 插件生成 Go 代码,以及解析 proto 文件来自动化生成服务骨架代码。