0%

kratos 源码分析 03:kratos CLI

这篇文章我们继续分析 kratos CLI 命令行工具的实现。我们将首先介绍 kratos CLI 工具的用法,之后再从源码层面分析其实现原理。

kratos CLI 概览

kratos CLI 是基于 cobra 框架构建的命令行工具,用于项目脚手架生成、Proto 文件管理和代码生成,用于简化 kratos 微服务的开发流程。

kratos CLI 共提供 5 个顶级命令:

1
2
3
4
5
6
7
8
9
kratos (root)
├── new 创建项目
├── proto Proto 文件操作
│ ├── add 添加 Proto 模板
│ ├── client 生成客户端代码
│ └── server 生成服务端实现
├── upgrade 升级工具
├── changelog 查看变更日志
└── run 运行项目
命令 简述 示例
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# kratos proto add api/helloworld/v1/demo.proto

# more api/helloworld/v1/demo.proto
syntax = "proto3";

package api.helloworld.v1;

option go_package = "cli-demo/api/helloworld/v1;v1";
option java_multiple_files = true;
option java_package = "api.helloworld.v1";

service Demo {
rpc CreateDemo (CreateDemoRequest) returns (CreateDemoReply);
rpc UpdateDemo (UpdateDemoRequest) returns (UpdateDemoReply);
rpc DeleteDemo (DeleteDemoRequest) returns (DeleteDemoReply);
rpc GetDemo (GetDemoRequest) returns (GetDemoReply);
rpc ListDemo (ListDemoRequest) returns (ListDemoReply);
}

message CreateDemoRequest {}
message CreateDemoReply {}

message UpdateDemoRequest {}
message UpdateDemoReply {}
......

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
2
3
4
5
6
7
8
# ls -l api/helloworld/v1/greeter*
-rw-r--r-- 1 root root 4368 May 19 12:03 api/helloworld/v1/greeter_grpc.pb.go
-rw-r--r-- 1 root root 2296 May 19 12:03 api/helloworld/v1/greeter_http.pb.go
-rw-r--r-- 1 root root 8073 May 19 12:03 api/helloworld/v1/greeter.pb.go
-rw-r--r-- 1 root root 676 May 19 11:52 api/helloworld/v1/greeter.proto

# ls -l openapi.yaml
-rw-r--r-- 1 root root 3039 May 19 12:03 openapi.yaml

如果检测到 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
2
# kratos proto server api/helloworld/v1/greeter.proto
# 生成 internal/service/greeter.go

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
2
3
4
5
# kratos run
2026/05/19 22:21:24 maxprocs: Leaving GOMAXPROCS=4: CPU quota undefined
DEBUG msg=config loaded: config.yaml format: yaml
INFO ts=2026-05-19T22:21:24+08:00 caller=http/server.go:330 service.id=OPS-3430 service.name= service.version= trace.id= span.id= msg=[HTTP] server listening on: [::]:8000
INFO ts=2026-05-19T22:21:24+08:00 caller=grpc/server.go:231 service.id=OPS-3430 service.name= service.version= trace.id= span.id= msg=[gRPC] server listening on: [::]:9000

整体架构

目录结构

kratos CLI 的源码位于 cmd/kratos/ 目录下,整体目录结构如下所示:

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
cmd/kratos/
├── main.go # 入口,注册所有命令
├── version.go # 版本号定义
└── internal/
├── project/ # kratos new 命令
│ ├── project.go # 命令定义、参数解析、流程控制
│ ├── new.go # 独立项目创建
│ └── add.go # 单体仓库子服务添加
├── proto/ # kratos proto 命令
│ ├── proto.go # 父命令定义
│ ├── add/ # kratos proto add 子命令
│ │ ├── add.go # 命令定义与参数解析
│ │ ├── proto.go # Proto 结构体与 Generate 方法
│ │ └── template.go # Proto 文件模板
│ ├── client/ # kratos proto client 子命令
│ │ └── client.go # protoc 命令构建与执行
│ └── server/ # kratos proto server 子命令
│ ├── server.go # proto 文件解析与代码生成
│ └── template.go # Go 服务端模板
├── upgrade/ # kratos upgrade 命令
│ └── upgrade.go # go install 批量安装
├── change/ # kratos changelog 命令
│ ├── change.go # 命令定义与参数解析
│ └── get.go # GitHub API 交互与解析
├── run/ # kratos run 命令
│ └── run.go # cmd/ 目录查找与 go run
└── base/ # 公共工具包
├── path.go # 路径与文件操作:kratosHome、copyDir、copyFile、Tree
├── repo.go # Git 仓库管理:Repo 结构体(Git 仓库管理)
├── mod.go # Go Module 操作:ModulePath、ModuleVersion、KratosMod
├── install.go # 工具安装:GoInstall
└── vcs_url.go # VCS URL 解析:ParseVCSUrl(URL 解析)

分层注册

入口文件 cmd/kratos/main.go 使用 cobra 框架定义根命令并注册子命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var rootCmd = &cobra.Command{
Use: "kratos",
Short: "Kratos: An elegant toolkit for Go microservices.",
Long: `Kratos: An elegant toolkit for Go microservices.`,
Version: release,
}

func init() {
rootCmd.AddCommand(project.CmdNew)
rootCmd.AddCommand(proto.CmdProto)
rootCmd.AddCommand(upgrade.CmdUpgrade)
rootCmd.AddCommand(change.CmdChange)
rootCmd.AddCommand(run.CmdRun)
}

func main() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}

命令注册采用分层结构

  1. 根级别main.goinit()):通过 rootCmd.AddCommand(...) 注册 5 个顶级命令
  2. 嵌套命令(如 proto):在 proto.goinit() 中注册子命令
1
2
3
4
5
6
7
8
9
10
11
12
// cmd/kratos/internal/proto/proto.go
var CmdProto = &cobra.Command{
Use: "proto",
Short: "Generate the proto files",
Long: "Generate the proto files.",
}

func init() {
CmdProto.AddCommand(add.CmdAdd)
CmdProto.AddCommand(client.CmdClient)
CmdProto.AddCommand(server.CmdServer)
}

每个命令的 Flags 在各自包的 init() 函数中注册。例如 kratos new 的 Flags 定义在 project.go 中:

1
2
3
4
5
6
7
8
9
10
func init() {
if repoURL = os.Getenv("KRATOS_LAYOUT_REPO"); repoURL == "" {
repoURL = "https://github.com/go-kratos/kratos-layout.git"
}
timeout = "60s"
CmdNew.Flags().StringVarP(&repoURL, "repo-url", "r", repoURL, "layout repo")
CmdNew.Flags().StringVarP(&branch, "branch", "b", branch, "repo branch")
CmdNew.Flags().StringVarP(&timeout, "timeout", "t", timeout, "time out")
CmdNew.Flags().BoolVarP(&nomod, "nomod", "", nomod, "retain go mod")
}

接下来我们详细分析每个命令的实现。

kratos new 实现

kratos new 是 kratos CLI 中最复杂的命令,核心逻辑分为两个分支:独立项目创建Project.New())和单体仓库子服务添加Project.Add())。该命令的整体实现流程如下:

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
用户输入 kratos new helloworld


解析 timeout → 创建带超时的 context


获取项目名称(参数或交互式输入)


processProjectParams() 处理项目路径
(支持 ~、相对路径、绝对路径)


┌─── nomod? ───┐
│ │
No Yes
│ │
▼ ▼
Project.New() 查找 go.mod 根目录


计算相对路径和 module 名


Project.Add()

独立项目创建

Project.New() 定义在 cmd/kratos/internal/project/new.go,流程如下:

  1. 检查目标目录:如果目录已存在,询问用户是否覆盖
  2. 创建 Repo 对象:从模板仓库 URL 创建 base.Repo 实例
  3. 复制模板:调用 repo.CopyTo() 克隆模板仓库到缓存,然后复制到目标目录,同时替换 Go module 名称
  4. 重命名 cmd/server:将 cmd/server 重命名为 cmd/<项目名>
  5. 显示文件树:调用 base.Tree() 展示所有创建的文件
  6. 输出后续步骤:打印 cdgo generatego build、运行等命令

单体仓库子服务添加

Project.Add() 定义在 cmd/kratos/internal/project/add.go,与 New() 的主要区别是:

  1. 忽略更多文件(apigo.modgo.sumthird_partyREADME.md 等)
  2. 使用 CopyToV2() 替代 CopyTo(),支持额外的字符串替换
  3. 子服务不会拥有独立的 api/,,让子服务指向根项目共享的 api/ 目录
1
2
3
pkgPath = fmt.Sprintf("%s/%s", mod, pkgPath)
err := repo.CopyToV2(ctx, to, pkgPath, repoAddIgnores,
[]string{filepath.Join(p.Path, "api"), "api"})

CopyToV2() 相比 CopyTo() 多了一个 replaces 参数,用于额外的字符串替换对。这里将 {子服务路径}/api 替换为 api,确保 import 路径正确。

这种 Add 方式执行的操作还是有点复杂,以下用一个具体场景逐步拆解假设目录结构如下:

1
2
3
4
/home/user/my-monorepo/        # projectRoot
├── go.mod # module github.com/myorg/my-monorepo
└── services/
└── order/ # 已有服务

用户在 /home/user/my-monorepo/services/ 下执行:

1
kratos new user --nomod

第 1 步:获取初始值

1
2
wd = os.Getwd()           // "/home/user/my-monorepo/services"
name = "user"

第 2 步:processProjectParams("user", wd)

1
2
3
4
5
// "user" 是相对路径,先转为绝对路径
_projectDir = filepath.Abs("user") // "/home/user/my-monorepo/services/user"
return filepath.Base(_projectDir), filepath.Dir(_projectDir)
// projectName = "user"
// workingDir = "/home/user/my-monorepo/services"

第 3 步:projectRoot = getgomodProjectRoot(workingDir)

/home/user/my-monorepo/services 向上查找 go.mod

1
2
/home/user/my-monorepo/services  → 无 go.mod → 继续
/home/user/my-monorepo → 有 go.mod → 停止
1
projectRoot = "/home/user/my-monorepo"

第 4 步:packagePath — 新服务相对于项目根的路径

1
2
3
packagePath = filepath.Rel(projectRoot, filepath.Join(workingDir, projectName))
// = filepath.Rel("/home/user/my-monorepo", "/home/user/my-monorepo/services/user")
// = "services/user"

第 5 步:mod — 从 go.mod 读取 module 名

1
2
mod = base.ModulePath("/home/user/my-monorepo/go.mod")
// = "github.com/myorg/my-monorepo"

第 6 步:p.Path — 服务的相对路径(用于 api 路径替换)

1
2
3
4
5
p.Path = filepath.Join(
strings.TrimPrefix(workingDir, projectRoot+"/"), // "services"
p.Name, // "user"
)
// = "services/user"

TrimPrefixworkingDirprojectRoot+"/" 的部分去掉,得到当前工作目录相对于项目根的相对路径,再拼接项目名。

第 7 步:p.Add() 中的 pkgPath 变换

1
2
pkgPath = fmt.Sprintf("%s/%s", mod, pkgPath)
// = "github.com/myorg/my-monorepo/services/user"

这是新服务在 Go module 中的完整 import 路径,将作为 CopyToV2 中替换模板 module 名的目标值。

第 8 步:CopyToV2 的替换对

1
2
3
4
5
6
7
8
replaces = []string{
原始module名, pkgPath,
filepath.Join(p.Path, "api"), "api",
}
// = []string{
"github.com/go-kratos/kratos-layout", "github.com/myorg/my-monorepo/services/user",
"services/user/api", "api",
}

经过 copyDircopyFile,所有文件中的字符串替换为:

替换 说明
github.com/go-kratos/kratos-layoutgithub.com/myorg/my-monorepo/services/user module 路径替换
services/user/apiapi 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
2
3
4
5
type Repo struct {
url string // 模板仓库 URL
home string // 缓存目录
branch string // 仓库分支
}
  • NewRepo() 构造函数计算缓存路径
  • repoDir() 函数从 URL 中提取缓存子目录
  • Path() 方法返回完整的缓存路径

例如 https://github.com/go-kratos/kratos-layout.git 的缓存路径为:

1
2
# ls  ~/.kratos/repo/github.com/go-kratos/kratos-layout@main
api cmd configs Dockerfile go.mod go.sum internal LICENSE Makefile openapi.yaml README.md third_party

Clone() 用于从远程仓库 clone 或者 pull:

1
2
3
4
5
6
7
8
9
10
11
12
13
func (r *Repo) Clone(ctx context.Context) error {
if _, err := os.Stat(r.Path()); !os.IsNotExist(err) {
return r.Pull(ctx) // 缓存已存在,执行 git pull 更新
}
// 首次克隆
var cmd *exec.Cmd
if r.branch == "" {
cmd = exec.CommandContext(ctx, "git", "clone", r.url, r.Path())
} else {
cmd = exec.CommandContext(ctx, "git", "clone", "-b", r.branch, r.url, r.Path())
}
...
}

复制与模块名替换

CopyTo() 方法完成从缓存到目标目录的复制,同时替换 Go module 名称:

1
2
3
4
5
6
7
8
9
10
func (r *Repo) CopyTo(ctx context.Context, to string, modPath string, ignores []string) error {
if err := r.Clone(ctx); err != nil {
return err
}
mod, err := ModulePath(filepath.Join(r.Path(), "go.mod"))
if err != nil {
return err
}
return copyDir(r.Path(), to, []string{mod, modPath}, ignores)
}

copyDir() 函数(定义在 base/path.go)递归复制目录,对所有文件执行字符串替换(:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func copyDir(src, dst string, replaces, ignores []string) error {
...
for _, fd := range fds {
if hasSets(fd.Name(), ignores) {
continue // 跳过忽略列表中的文件/目录
}
if fd.IsDir() {
e = copyDir(srcfp, dstfp, replaces, ignores) // 递归
} else {
e = copyFile(srcfp, dstfp, replaces) // 复制文件
}
}
...
}
  • 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.gorun() 函数解析用户输入的路径,提取出各组成部分,包括文件名、包名等
  • 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.protoUserhello_world.protoHelloWorld

template.go 定义了 proto 文件模板,使用 Go 标准库 text/template 渲染,模板生成 5 个标准 CRUD RPC 方法和 10 个空消息体(Request/Reply 对):

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

package {{.Package}};

option go_package = "{{.GoPackage}}";
option java_multiple_files = true;
option java_package = "{{.JavaPackage}}";

service {{.Service}} {
rpc Create{{.Service}} (Create{{.Service}}Request) returns (Create{{.Service}}Reply);
rpc Update{{.Service}} (Update{{.Service}}Request) returns (Update{{.Service}}Reply);
rpc Delete{{.Service}} (Delete{{.Service}}Request) returns (Delete{{.Service}}Reply);
rpc Get{{.Service}} (Get{{.Service}}Request) returns (Get{{.Service}}Reply);
rpc List{{.Service}} (List{{.Service}}Request) returns (List{{.Service}}Reply);
}

message Create{{.Service}}Request {}
message Create{{.Service}}Reply {}
// ...

最后 proto.goGenerate() 方法创建目录并将模板渲染后的内容写入新生成的 proto 文件中。

kratos proto client

kratos proto client 的实现在 cmd/kratos/internal/proto/client/client.go,它的核心是构建 protoc 命令行并作为其作为子进程执行。虽然命令名为 client,但它实际上会调用所有 protoc 插件,对 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
用户输入 kratos proto client helloworld.proto


检查 protoc 插件是否存在
(protoc-gen-go, protoc-gen-go-grpc, ...)

┌────┴────┐
存在 不存在
│ │
│ ▼
│ 自动执行 kratos upgrade
│ │
└────┬────┘


┌─── 输入类型 ───┐
│ │
.proto文件 目录
│ │
▼ ▼
generate() walk() 递归扫描
(跳过 third_party/)


generate()

generate() 函数是该命令的核心,它构建完整的 protoc 命令行:

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
func generate(proto string, args []string) error {
input := []string{
"--proto_path=.", // 当前目录
}
// 用户的 proto_path(默认 ./third_party)
if pathExists(protoPath) {
input = append(input, "--proto_path="+protoPath)
}
inputExt := []string{
"--proto_path=" + base.KratosMod(), // kratos 模块路径
"--proto_path=" + filepath.Join(base.KratosMod(), "third_party"), // kratos third_party
"--go_out=paths=source_relative:.", // Go protobuf 代码
"--go-grpc_out=paths=source_relative:.", // gRPC 代码
"--go-http_out=paths=source_relative:.", // HTTP 代码
"--go-errors_out=paths=source_relative:.", // 错误代码
"--openapi_out=paths=source_relative:.", // OpenAPI 文档
}
input = append(input, inputExt...)

// 检测 validate.proto 的引用
protoBytes, err := os.ReadFile(proto)
if ok, _ := regexp.Match(`\n[^/]*(import)\s+"validate/validate.proto"`, protoBytes); ok {
input = append(input, "--validate_out=lang=go,paths=source_relative:.")
}

input = append(input, proto) // proto 文件
// 添加用户额外参数
for _, a := range args {
if strings.HasPrefix(a, "-") {
input = append(input, a)
}
}

fd := exec.Command("protoc", input...)
fd.Stdout = os.Stdout
fd.Stderr = os.Stderr
fd.Dir = "."
return fd.Run()
}
  • --proto_path 层次当前目录用户 third_partyKratos 模块路径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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func KratosMod() string {
// 获取 GOMODCACHE 路径
cacheOut, _ := exec.Command("go", "env", "GOMODCACHE").Output()
cachePath := strings.Trim(string(cacheOut), "\n")
// 备用:从 GOPATH 推导
pathOut, _ := exec.Command("go", "env", "GOPATH").Output()
gopath := strings.Trim(string(pathOut), "\n")
if cachePath == "" {
cachePath = filepath.Join(gopath, "pkg", "mod")
}
// 获取 kratos 版本号
if path, err := ModuleVersion("github.com/go-kratos/kratos/v2"); err == nil {
return filepath.Join(cachePath, path)
}
// GOPATH 模式(旧方式)
return filepath.Join(gopath, "src", "github.com", "go-kratos", "kratos")
}

ModuleVersion() 通过 go mod graph 命令获取 kratos 的依赖版本:

1
2
3
4
5
6
7
8
func ModuleVersion(path string) (string, error) {
fd := exec.Command("go", "mod", "graph")
...
// 找到 "github.com/go-kratos/kratos/v2@v2.7.2" 这样的行
if strings.Contains(str, path+"@") && i != -1 {
return path + str[i:], nil
}
}

最终路径形如:/root/go/pkg/mod/github.com/go-kratos/kratos/v2@v2.7.2。该路径下包含 kratos 提供的 proto 文件:

1
2
3
4
5
6
kratos/v2@v2.7.2/
├── api/metadata/metadata.proto ← import "api/metadata/metadata.proto"
└── third_party/
├── google/api/annotations.proto ← import "google/api/annotations.proto"
├── validate/validate.proto ← import "validate/validate.proto"
└── errors/errors.proto ← import "errors/errors.proto"

这里解释再解释下 --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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用户输入 kratos proto server api/helloworld/v1/greeter.proto


读取并解析 proto 文件(emicklei/proto 解析器)


proto.Walk() 遍历定义
├── 提取 go_package 选项
└── 提取 Service 和 RPC 方法定义


确定每个 RPC 的流类型(unary/client-stream/server-stream/bidi-stream)


渲染 Go 模板,生成服务端骨架代码


写入 internal/service/<service>.go

getMethodType() 根据 StreamsRequestStreamsReturns 两个布尔值判断 RPC 方法类型:

1
2
3
4
5
6
7
8
9
10
11
12
func getMethodType(streamsRequest, streamsReturns bool) MethodType {
if !streamsRequest && !streamsReturns {
return unaryType // 1: 普通 RPC
} else if streamsRequest && streamsReturns {
return twoWayStreamsType // 2: 双向流
} else if streamsRequest {
return requestStreamsType // 3: 客户端流
} else if streamsReturns {
return returnsStreamsType // 4: 服务端流
}
return unaryType
}

template.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
var serviceTemplate = `
package service

import (
{{- if .UseContext }}
"context"
{{- end }}
{{- if .UseIO }}
"io"
{{- end }}

pb "{{ .Package }}"
{{- if .GoogleEmpty }}
"google.golang.org/protobuf/types/known/emptypb"
{{- end }}
)

type {{ .Service }}Service struct {
pb.Unimplemented{{ .Service }}Server
}

func New{{ .Service }}Service() *{{ .Service }}Service {
return &{{ .Service }}Service{}
}

{{ range .Methods }}
{{- if eq .Type 1 }}
// Unary: func (s *XxxService) Method(ctx context.Context, req *pb.Req) (*pb.Reply, error)
{{- else if eq .Type 2 }}
// 双向流: func (s *XxxService) Method(conn pb.Xxx_MethodServer) error
{{- else if eq .Type 3 }}
// 客户端流: func (s *XxxService) Method(conn pb.Xxx_MethodServer) error
{{- else if eq .Type 4 }}
// 服务端流: func (s *XxxService) Method(req *pb.Req, conn pb.Xxx_MethodServer) error
{{- end }}
{{- end }}
`

模板的智能之处在于:

  1. 按需导入:只有在存在 unary 方法时才导入 context,只有在存在流式方法时才导入 io
  2. Empty 类型处理:如果请求或返回类型是 google.protobuf.Empty,则使用 emptypb.Empty 替代
  3. 流式方法骨架:自动生成 Recv/Send/SendAndClose 的循环骨架代码

kratos upgrade 实现

kratos upgrade 的实现非常简洁,在 cmd/kratos/internal/upgrade/upgrade.go 中,直接调用 base.GoInstall() 安装 6 个工具

1
2
3
4
5
6
7
8
9
10
11
func Run(_ *cobra.Command, _ []string) {
err := base.GoInstall(
"github.com/go-kratos/kratos/cmd/kratos/v2@latest",
"github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest",
"github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latest",
"google.golang.org/protobuf/cmd/protoc-gen-go@latest",
"google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest",
"github.com/google/gnostic/cmd/protoc-gen-openapi@latest",
)
...
}

GoInstall() 定义在 base/install.go,逐个执行 go install 命令。

kratos changelog 实现

kratos changelog 的实现在 cmd/kratos/internal/change/ 目录下,通过 GitHub REST API 获取版本信息。

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
用户输入 kratos changelog [dev | 版本号]


ParseGithubURL() 解析 owner 和 repo


┌─── version ───┐
│ │
"dev" 其他/默认"latest"
│ │
▼ ▼
GetCommitsInfo() GetReleaseInfo()
│ │
│ 获取最新 release │ 获取指定版本的
│ 的发布时间 │ 发布信息
│ │
│ 分页获取该时间 │
│ 之后的所有 commit│
│ │
▼ ▼
ParseCommitsInfo() ParseReleaseInfo()
│ │
│ 按 commit 前缀 │ 输出 Author、
│ 分类输出 Markdown│ Date、URL、Body
└────────┬────────┘


输出到终端
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
用户输入 kratos run [-- -conf ./configs]


splitArgs() 分隔命令参数和程序参数
(以 -- 为分隔符)


┌─── 指定了目录? ───┐
│ │
Yes No
│ │
│ ▼
│ findCMD() 查找 cmd/ 子目录
│ │
│ ┌─── 找到几个? ───┐
│ │ │
│ 0个 1个 多个
│ │ │ │
│ 报错 直接使用 交互式选择
│ │ │ │
└───────────┴──────┴─────────┘


go run <dir> <programArgs...>

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_worldHelloWorld

小结

在这篇文章中,我们详细分析了 kratos CLI 命令行工具的实现原理。我们重点解析了它如何将一个 GitHub 仓库作为模板,并通过模板渲染、Go 模块替换等操作,最终生成一个真实的用户项目。此外,我们还学习了如何根据 Proto 文件生成样本代码,包括调用 protoc 插件生成 Go 代码,以及解析 proto 文件来自动化生成服务骨架代码。