- 区块链
【免费下载链接】cosmos-sdk
Framework for building performant, customizable blockchains with native interoperability
导读
在 Cosmos SDK 中,一个业务模块(Module)的代码组织方式直接决定了其可维护性、可测试性与可组合性。本文以官方文档《Recommended Folder Structure》为核心,系统讲解 Cosmos SDK 推荐的标准模块目录结构:包括proto/下的消息与服务定义组织,以及x/{module_name}下根目录、client/、exported/、keeper/、module/、simulation/各子目录的职责划分。读完本文,你将掌握如何规划一个新模块的文件布局、如何通过expected_keepers.go接口契约解耦模块间依赖,并了解每类文件的源码级职责与真实落地样例。
目录结构总览:官方推荐的标准布局
官方文档明确说明,以下结构是**建议性(suggestion)**而非强制规范,应用开发者被鼓励在此基础上改进并回馈社区。一个典型模块由两大块组成:协议缓冲区(Protocol Buffers)定义所在的proto目录,以及 Go 实现所在的x/{module_name}目录。
proto 目录:定义链上数据结构与服务接口
官方推荐将 proto 文件按如下路径组织:
proto └── {project_name} └── {module_name} └── {proto_version} ├── {module_name}.proto ├── event.proto ├── genesis.proto ├── query.proto └── tx.proto各文件职责如下:
| 文件 | 职责 |
|---|---|
{module_name}.proto | 模块的通用消息类型定义 |
event.proto | 与事件相关的消息类型定义 |
genesis.proto | 与创世状态(genesis state)相关的消息类型定义 |
query.proto | 模块的 Query 服务及相关的消息类型定义 |
tx.proto | 模块的 Msg 服务及相关的消息类型定义 |
以当前仓库为例,x/bank(银行模块)的 proto 定义位于 proto/cosmos/bank/v1beta1,包含bank.proto、event相关定义、genesis.proto、query.proto、tx.proto和authz.proto等文件;x/staking(质押模块)对应 proto/cosmos/staking/v1beta1,包含staking.proto、genesis.proto、query.proto、tx.proto、authz.proto。可见v1beta1即文档中的{proto_version}位置。此外,模块自身的配置(如 depinject 所需的 module config)还可放在 proto/cosmos/bank/module/v1/module.proto 这类module/{version}路径下。
这些*.proto文件经由 proto/buf.gen.gogo.yaml 与 proto/buf.gen.pulsar.yaml 中配置的代码生成工具(protocgen 脚本参见 scripts/protocgen.sh、scripts/protocgen-pulsar.sh),生成对应的*.pb.go文件,供 Go 代码使用。
x/{module_name} 目录:模块的 Go 实现
官方推荐的 Go 侧结构如下:
x/{module_name} ├── client │ ├── cli │ │ ├── query.go │ │ └── tx.go │ └── testutil │ ├── cli_test.go │ └── suite.go ├── exported │ └── exported.go ├── keeper │ ├── genesis.go │ ├── grpc_query.go │ ├── hooks.go │ ├── invariants.go │ ├── keeper.go │ ├── keys.go │ ├── msg_server.go │ └── querier.go ├── module │ └── module.go │ └── abci.go │ └── autocli.go ├── simulation │ ├── decoder.go │ ├── genesis.go │ ├── operations.go │ └── params.go ├── {module_name}.pb.go ├── codec.go ├── errors.go ├── events.go ├── events.pb.go ├── expected_keepers.go ├── genesis.go ├── genesis.pb.go ├── keys.go ├── msgs.go ├── params.go ├── query.pb.go ├── tx.pb.go └── README.md需要说明的是:官方文档中的module/子目录在当前仓库的模块实现中一般直接并入根级module.go(如 x/bank/module.go、x/staking/module.go),BeginBlocker/EndBlocker逻辑也常直接放在 keeper 层(如 x/staking/keeper/abci.go)。这是社区在实际演进中对建议结构的合理适配,下文将按职责逐一讲解。
根目录文件:类型、编解码与模块契约
根目录承载模块最核心的类型定义与编解码逻辑,其中多数文件由 proto 生成,其余为手写契约代码。
由 Protocol Buffers 生成的文件(*.pb.go)
{module_name}.pb.go:模块通用消息类型的生成代码;events.pb.go:事件消息类型;genesis.pb.go:创世状态类型;query.pb.go:Query 服务相关类型;tx.pb.go:Msg 服务相关类型。
以x/bank为例,x/bank/types 下即包含bank.pb.go、genesis.pb.go、query.pb.go、tx.pb.go、authz.pb.go等生成文件。
手写的类型与工具文件
| 文件 | 职责(依据官方文档并结合源码验证) |
|---|---|
codec.go | 模块接口类型的注册方法。参见 x/bank/types/codec.go,其中将MsgSend、MsgMultiSend、SendAuthorization等注册进InterfaceRegistry,供 Amino/JSON 编解码使用 |
errors.go | 模块的哨兵错误(sentinel errors),如 x/bank/types/errors.go 中定义的ErrNoInputs、ErrSendDisabled等,统一通过errorsmod.Register注册到 ABCI 错误码空间 |
events.go | 模块的事件类型及构造函数,如 x/bank/types/events.go 中的NewCoinReceivedEvent、NewCoinSpentEvent等 |
expected_keepers.go | 模块的期望 keeper 接口契约(详见下文"解耦设计"小节) |
genesis.go | 创世状态的默认值、校验与转换方法,如 x/bank/types/genesis.go 中的DefaultGenesisState()与Validate() |
keys.go | 模块 store key 及相关辅助函数,如 x/bank/types/keys.go 定义了StoreKey = "bank"以及余额(BalancesPrefix)、总供给(SupplyKey)、denom 元数据、SendEnabled 等 store 前缀常量 |
msgs.go | 模块消息类型定义及其方法(ValidateBasic、GetSigners等),如 x/bank/types/msgs.go 中的MsgSend、MsgMultiSend |
params.go | 模块参数类型定义及关联方法,如 x/bank/types/params.go 中的Params与SendEnabled |
此外,x/staking还展示了两个典型补充:exported.go(见下节)与hooks.go(x/staking/types/hooks.go),后者定义了质押模块对外广播的 hook 接口,供其他模块订阅"委托/解委托/验证人变更"等事件。
expected_keepers.go 与 exported/:模块间解耦的两板斧
这是官方文档着重强调的设计模式,也是 Cosmos SDK 模块化思想的精髓所在。
为什么需要接口契约
如果模块 A 要使用模块 B 的 Keeper,直接 import 模块 B 会造成强耦合,甚至引发 import cycle。官方推荐的方案是:模块 A 在expected_keepers.go中声明一个最小的接口,只列出自己需要的方法,并在运行时接收模块 B 的 keeper 实例。
以x/bank为例,x/bank/types/expected_keepers.go 声明了AccountKeeper接口——它只暴露银行模块真正需要的账户方法(GetAccount、SetAccount、NewAccount、模块账户相关方法、AddressCodec()等),而不要求完整的 x/auth Keeper。x/bank/keeper/keeper.go中的BaseKeeper结构体正是持有ak types.AccountKeeper这一接口字段来访问账户信息。
exported/:提供规范化的跨模块类型
文档指出:接口契约中的方法可能会操作或返回"由实现该 keeper 的模块特有"的类型,此时就需要exported/出场。exported/中定义的类型使用规范化(canonical)类型,使模块可以通过expected_keepers.go的接口契约接收 keeper,同时保持代码 DRY(Don't Repeat Yourself)并避免 import cycle 混乱。
实际案例:
- x/bank/exported/exported.go 定义了
GenesisBalance接口(GetAddress()/GetCoins()),供其他模块以通用方式读取创世余额; - 当前仓库中
x/staking的规范化类型位于 x/staking/types/exported.go,定义了DelegationI与ValidatorI接口——后者列出了验证人的规范化访问方法(GetMoniker()、IsBonded()、GetConsensusPower(math.Int) int64、TokensFromShares(...)等)。该文件在官方结构图中对应x/{module_name}/exported/exported.go,从源码结构看,将exported.go置于独立的exported/包内(而非模块types包内)正是为了减少依赖面、避免引入math之外的重型依赖。
keeper/ 子目录:状态读写与消息处理的核心
keeper/目录承载模块的Keeper与MsgServer实现,是模块状态机的核心。官方推荐的文件分工如下:
| 文件 | 职责 |
|---|---|
keeper.go | Keeper结构体与构造函数、核心状态读写方法 |
msg_server.go | MsgServer实现,处理各Msg消息 |
grpc_query.go | gRPC Query 服务实现 |
genesis.go | 创世状态的初始化(InitGenesis)与导出(ExportGenesis) |
hooks.go | 模块 hook 的调用与分发逻辑 |
invariants.go | 不变量检查(invariants),用于链上一致性断言 |
keys.go | store key 与集合(collections)对象定义 |
querier.go | 早期风格的 querier(现代模块通常由 grpc_query.go 取代) |
Keeper 接口与实现分离
以 x/bank/keeper/keeper.go 为例:文件顶部先声明Keeper接口(组合SendKeeper、创世方法、供给查询、denom 元数据、模块间转账SendCoinsFromModuleToAccount/MintCoins/BurnCoins/DelegateCoins/UndelegateCoins、虚拟账户相关方法以及types.QueryServer),再由BaseKeeper具体实现,并通过var _ Keeper = (*BaseKeeper)(nil)做编译期断言。这种"接口 + 实现"的写法让模块可插拔、可 mock。
MsgServer:消息的入口校验与执行
x/bank/keeper/msg_server.go 展示了MsgSend的处理链路:先通过AddressCodec解析 from/to 地址,校验Amount.IsValid()与IsAllPositive(),再检查IsSendEnabledCoins与BlockedAddr(to)(禁止向黑名单地址转账),最后调用SendCoins完成转账并埋点 telemetry。它通过NewMsgServerImpl(keeper)构造,并实现types.MsgServer接口。
gRPC Query 服务
x/staking/keeper/grpc_query.go 展示了查询服务的实现模式:以Querier结构体组合*Keeper,实现types.QueryServer,并通过runtime.KVStoreAdapter(k.storeService.OpenKVStore(ctx))访问状态。x/bank的对应实现位于 x/bank/keeper/grpc_query.go,其中大量使用collections包进行分页查询(如GetPaginatedTotalSupply)。
BeginBlocker 与 EndBlocker
官方文档指出module/abci.go用于定义BeginBlocker/EndBlocker(仅在确实需要时编写)。当前仓库中 x/staking/keeper/abci.go 即包含BeginBlocker(持久化历史 header 与验证人集合、按HistoricalEntries参数修剪旧条目)与EndBlocker(调用BlockValidatorUpdates更新验证人集合)的实现;x/bank/module.go中也通过实现appmodule.HasEndBlocker接口注册了EndBlock。
module/(module.go):AppModule 与 AppModuleBasic
模块的AppModule与AppModuleBasic是模块接入应用(App)的门面。以 x/bank/module.go 为例:
AppModuleBasic负责静态能力:模块名(Name())、Legacy Amino 编解码注册、默认创世状态(DefaultGenesis)、创世校验(ValidateGenesis)、gRPC Gateway 路由、CLI 命令(GetTxCmd)、接口注册(RegisterInterfaces);AppModule组合AppModuleBasic并实现appmodule.AppModule、module.HasGenesis、module.HasServices等接口,在RegisterServices中注册MsgServer与QueryServer(调用types.RegisterMsgServer/types.RegisterQueryServer);- 文件头部还定义了
ConsensusVersion = 4,用于模块升级时的共识版本迁移判定。
autocli.go:从 proto 自动生成 CLI
官方文档中的module/autocli.go承载模块的 autocli 选项。autocli 可以根据 proto 定义自动生成查询与交易命令,模块只需声明少量定制。以 x/bank/autocli.go 为例,其通过AutoCLIOptions()返回autocliv1.ModuleOptions,将Balance、AllBalances、SpendableBalances、TotalSupply、SupplyOf、Params等 RPC 映射为simd query bank balance [address] [denom]这类具体命令(Use、Short、PositionalArgs字段)。自定义 CLI 的完整实现则可参考 x/bank/client/cli/tx.go,其中NewSendTxCmd生成simd tx bank send [from] [to] [amount],NewMultiSendTxCmd生成simd tx bank multi-send ...,并支持--split标志将金额平均分发给多个地址。
client/ 子目录:CLI 命令与测试套件
client/cli/query.go:模块查询命令(如余额、供给、参数查询);client/cli/tx.go:模块交易命令(如转账);client/testutil/:CLI 测试套件,通常包含cli_test.go与suite.go。
当前仓库x/bank/client/cli/下为tx.go与tx_test.go,x/staking/client/cli/下则同时包含查询与交易命令。这些 CLI 命令由AppModuleBasic.GetTxCmd()汇总到simd二进制中。从 x/bank/client/cli/tx.go 的源码看,命令遵循统一模式:用client.GetClientTxContext(cmd)获取客户端上下文、解析参数、构造Msg,最后调用tx.GenerateOrBroadcastTxCLI完成签名与广播;cobra.MinimumNArgs/cobra.ExactArgs保证参数个数校验。
simulation/ 子目录:区块链模拟器支持
simulation/包为区块链模拟器(simapp)提供确定性仿真所需的函数。官方推荐结构包含:
decoder.go:解码模拟中的操作数据;genesis.go:生成随机的创世状态;operations.go:定义模拟操作(如随机转账、随机质押);params.go:模拟参数。
当前仓库的x/bank/simulation/下有genesis.go、msg_factory.go、operations.go、proposals.go及对应测试文件;simapp通过AppModuleSimulation接口集成这些函数(参见 x/bank/module.go 中的_ module.AppModuleSimulation = AppModule{}断言)。模拟器的详细设计参见 14-simulator.md。
根目录 README.md:模块规格说明书
每个模块根目录应有README.md,作为模块规格文档,概述重要概念、状态存储结构以及消息与事件类型定义(如 x/bank/README.md、x/staking/README.md)。如何编写模块规格的详细指导见 docs/spec/SPEC_MODULE.md。
与 Keeper 设计文档的衔接
目录结构中大量文件(keeper.go、expected_keepers.go、keys.go、genesis.go等)都与"Keeper 设计"一脉相承:expected_keepers.go中的接口契约正是 06-keeper.md 的 Type Definition 小节所强调的依赖注入方式。建议在动手搭建模块目录前,先通读 00-intro.md 至 16-testing.md 的模块开发系列文档,形成完整的开发认知。
设计要点总结
- 主次分明:proto 目录只做类型与服务定义,Go 代码负责实现,两者通过
*.pb.go生成代码衔接。 - 依赖反转:通过
expected_keepers.go的最小接口 +exported/的规范化类型,实现模块间"面向接口编程",避免直接依赖与 import cycle。 - 职责单一:
keeper/(状态与消息处理)、client/(CLI)、module/(接入 App)、simulation/(仿真)各司其职,测试文件与实现文件就近放置。 - 文件即文档:根目录
README.md是模块规格的入口,errors.go、events.go、keys.go等命名本身即可传达模块的结构语义。 - 规范是建议而非教条:官方结构是社区最佳实践的沉淀,实际模块(如
x/staking将 exported 类型放入types/exported.go、x/bank将 abci 逻辑放入 keeper)可以在保持职责清晰的前提下灵活调整。
- 区块链
【免费下载链接】cosmos-sdk
Framework for building performant, customizable blockchains with native interoperability
相关推荐
Swift项目结构规范:Style Guide推荐的目录组织和文件管理
Swift项目结构规范:Style Guide推荐的目录组织和文件管理 Swift编程语言在iOS和macOS开发中占据重要地位,而良好的项目结构和文件管理规范
代码质量教程Ghost Downloader 3:6 种下载协议一个工具搞定,附完整上手指南
Ghost Downloader 3:6 种下载协议一个工具搞定,附完整上手指南 Ghost Downloader 3 是一个用 Python + Qt 写的跨
桌面应用网络Jina Executor 文件结构详解:从单文件到多模块 Python 包的组织规范
Jina Executor 文件结构详解:从单文件到多模块 Python 包的组织规范 本指南围绕 Jina 中 Executor 的 py_modules 加
后端人工智能模型推理服务微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考