打造千万级流量秒杀第十八课 项目规范:Go 项目初始化都有哪些规范?

发布于:2023-01-16 ⋅ 阅读:(599) ⋅ 点赞:(0)

上一讲,我们已经做好技术选型了,确定选择 Go 语言来实现秒杀系统。接下来我们就开始进入代码开发阶段。不过在此之前,我们还需要了解下 Go 项目规范都有哪些。

为什么呢?有句很经典的话是这么说的 “代码是给机器运行的,但首先它是给人看的”。也就是说,在你将项目发布到服务器上运行前,该项目相关的人员可能都会看它的代码。假如没有规范,在审核代码的时候就没有评判标准,也就无法知道代码的质量如何。

除了代码规范,拥有一定复杂度的项目,通常会有多个子目录,还会有配置文件和多个命令行参数,这些也需要规范。基于此,这一讲我将按照从外到内,从目录结构到命令行参数的顺序,和你聊聊 Go 项目规范。

目录结构规范

很多人会觉得奇怪,目录结构有什么好介绍的,不就是创建个目录嘛?其实,这里面有很大的讲究。因为,规划好目录结构是初始化一个项目的第一步。

众所周知,很多语言都可以通过引入依赖包,也就是公共库的方式来快速实现功能,也可以通过将代码导出为依赖包,实现代码复用。比如 Java、Python、PHP 都有自己的依赖包管理器, Go 也类似。通常 Go 的包名称跟代码所在目录名称是一样的。如果目录结构规划得不好,会出现什么问题呢?

首先,可能会存在依赖关系混乱的问题,比如循环依赖和反向依赖。

什么是循环依赖呢?举个例子,位于同层级中的 A、B、C 三个包,A 依赖 B,B 依赖 C,结果后面写代码的时候发现 C 还要依赖 A,这就造成循环依赖了。在 Go 语言中,循环依赖会导致项目编译失败,编译器会提醒你解决循环依赖的问题。

反向依赖又是怎样的呢?比如 A 和 C 位于第一层,B 和 D 位于第二层,正常情况下 A 依赖 B,C 依赖 D,如果 B 依赖了 C,就构成反向依赖,导致层级关系错乱。

其次,可能会导致阅读代码的时候非常不方便。

假如项目中有 100 个源代码文件,如果都放到一个目录下,用 IDE 看代码的时候,文件列表将会变得很长,需要上下滚动才能找到想看的文件。这就好比你的电脑桌面铺满了各种文件,要从中找出想用的文件将会很费时。

这两个问题该如何解决呢?其实项目的目录结构有一些规范可遵循。通常,可编译运行的开源 Go 项目倾向于使用下面的目录结构:

├── cmd/
├── config/
├── docs/
├── internal/
├── pkg/
├── script/
├── static/
├── go.mod
├── README.md
├── LICENSE.md
├── Makefile

其中 cmd 目录用于存放可执行程序的入口源码文件,config 目录存放配置文件,docs 目录存放文档。internal 目录中存放的是仅用于项目内部的源码,而 pkg 目录存放的则是可以被其他项目引用的源码。script 目录用于存放脚本文件,比如用于编译程序的脚本文件。static 目录中存放的是诸如网页源文件或者图片之类的静态资源文件。

除了以上几个目录外,Go 项目根目录中还有几个常见的文件,主要是 go.mod、README.md、LICENSE.md 和 Makefile 。

其中,go.mod 用于管理外部依赖关系,通常通过go mod init命令生成。假如你的代码中引用了 github.com/hello/world 包,你需要在 go.mod 中添加对这个包的依赖关系。如果你使用 Goland 开发 Go 项目,Goland 将会帮你自动更新 go.mod 文件。注意,别忘了在 Goland 中开启 Go Modules 功能,配置如下:

Drawing 0.png

README.md 和 LICENSE.md 一个是说明文档的入口文件,一个是源码分发许可文件。 最后一个 Makefile 则是执行 make 命令时的配置文件,常用于执行编译、单元测试、运行等操作。

除了开源项目的规范外,还有其他规范吗?有!那就是之前给你介绍的 DDD,它主要用于业务系统项目。如果你的项目按照 DDD 来设计,那么项目的目录结构要体现出 DDD 的思想。

还记得 DDD 的分层设计吗?如果按照 DDD 分层设计,那么目录结构中需要包含以下内容:

├── interfaces/
├── application/
├── domain/
├── infrastructure/

其中infrastructure存放基础设施相关的代码,如秒杀系统中的通用函数、用于访问 Redis 和 MySQL 的代码等。它位于 DDD 分层设计中的最底层,其他层都可以依赖它。

domain中存放的是领域的定义和核心逻辑,类似于 MVC 模型中的 Model,比如秒杀系统中的活动信息定义和活动相关的处理逻辑。它可以引用 infrastructure 从存储中获取数据并进行处理,也可以从上层接收并处理数据,给上层返回处理结果。

application用于存放请求数据的整体控制逻辑,有点类似于 MVC 模型中的 controller,比如秒杀接口服务中获取活动信息的入口函数。它主要接收上游请求,并控制数据的整体处理流程。

interfaces目录主要存放接口定义和初始化代码,比如秒杀接口服务中 HTTP 接口定义和路由初始化逻辑。它主要引用 application,将请求转给 application 处理。

在秒杀项目中,我们将开源 Go 项目和 DDD 项目的特点结合起来,使用 cmd 目录存放秒杀服务的入口代码,提供 api、admin、bench 等子命令。

最终,秒杀系统的目录结构大致如下:

├── README.md
├── go.mod
├── LICENSE
├── Makefile
├── main.go
├── cmd/
│   ├── api.go
│   ├── admin.go
│   ├── bench.go
│   └── root.go
├── interfaces/
│   ├── admin/
│   ├── api/
│   └── rpc/
├── application/
│   ├── api/
│   └── admin/
├── domain/
│   ├── event/
│   ├── user/
│   ├── product/
│   └── stock/
├── infrastructure/
│   ├── utils/
│   ├── logger/
│   ├── metrics/
│   ├── redis/
│   ├── mysql/
│   ├── etcd/
│   └── kafka/
├── config
│   ├── seckill.ini
│   └── seckill.toml
└── docs
    ├── api.md
    ├── admin.md
    └── bench.md

命令行参数规范

前面我提到了使用 cmd 目录存放秒杀程序入口代码,用于管理秒杀程序的子命令。那么,秒杀服务命令行参数应该怎么设计呢?

我们先看下其他程序的命令行参数是如何的。以我电脑上的一个 Go 程序 cobra 为例子,执行 ./cobra -h 后,结果如下:

Drawing 1.png

从截图上可以看到,输出的结果中主要包括三部分:Usage、Available Commands、Flags。其中 Usage 是告诉你该命令总的用法, Available Commands 告诉你有哪些子命令,Flags 则是告诉你可以用哪些参数。

看完后你有没有觉得整个输出结果让人一目了然?

总的来说,一个优秀的程序命令由三部分组成:主命令、子命令、参数。主命令是整个程序的入口,子命令是程序内各种主要的功能,参数则是告诉程序如何执行这些功能。

对应到秒杀服务中,主命令应该是seckill。秒杀服务包括秒杀接口服务和秒杀管理后台后端,它们的子命令分别是apiadmin。另外,为了方便做一些比较复杂的压力测试,我们还可以提供一个bench命令。

这些子命令如何管理呢?这里我给你推荐一款非常好用的工具cobra,用它可以生成解析命令行参数的代码。你可以执行go get -u github.com/spf13/cobra安装该工具。

我们该如何使用 cobra 命令生成代码呢?在项目根目录下执行以下命令:

cobra init seckill
cobra add api
cobra add admin
cobra add bench

我们就获得了 cmd/root.go、cmd/api.go、cmd/admin.go、cmd/bench.go。他们分别是主命令 seckill、子命令 api、admin、bench 的入口文件。
打开 cmd/root.go,你将看到如下两段代码

var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
   Use:   "seckill",
   Short: "A brief description of your application",
   Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
   // Uncomment the following line if your bare application
   // has an action associated with it:
   //Run: func(cmd *cobra.Command, args []string) { },
}
func init() {
   cobra.OnInitialize(initConfig)
   // Here you will define your flags and configuration settings.
   // Cobra supports persistent flags, which, if defined here,
   // will be global for your application.
   rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.seckill.yaml)")
   // Cobra also supports local flags, which will only run
   // when this action is called directly.
   rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

其中 rootCmd 是主命令的配置,类型是 *cobra.Command。它的 Use 字段是主命令名称,Short 字段是对命令的简短描述,Long 字段是对命令的详细描述。Short 字段和 Long 字段目前还是默认值,需要修改成秒杀命令的描述。

需要注意的是,rootCmd 中的 Run 字段默认是被注释掉的。该字段是 cobra 解析完命令行参数后正式执行功能的入口函数,你可以开启它,并在其中根据参数做一些初始化的工作。

在 init 函数中,cobra 会执行配置的初始化,并解析命令行参数。在生成的代码中,已经提供了 config 参数,用于指定配置文件路径。通常,命令行参数有长参数和短参数这两种格式,这里的 config 是长参数,在使用的时候是 --config 这种形式,而后面的 c 是短参数,使用的时候是 -c 这种形式。

cmd 目录下的 api.go、admin.go、bench.go 也类似,区别在于各子命令的 cmd 配置中 Run 字段是有个默认函数的。并且,在 init 函数中,会有类似 rootCmd.AddCommand(adminCmd) 的代码将子命令的配置添加到主命令的配置中。

接下来,我们可以在项目的根目录下添加 Makefile,用于编译程序。内容如下:

all: build
build:
   go build -o seckill main.go
clean:
   rm seckill
.PHONY: clean build all

其中 all 指令是 Makefile 的入口,也是执行 make 命令的默认行为,它依赖了 build 指令。build 指令用于编译出可执行文件 seckill ,而 clean 指令则是用于清理编译好的 seckill 文件。 .PHONY 是做什么用的呢?由于 Makefile 中依赖关系默认都是文件间的依赖,因此当目录中存在与 Makefile 中指令相同的文件时,可能会导致 make 命令无法正常执行 Makefile 中的指令,而 .PHONY 则是为了解决这个问题。

有了 Makefile 后,我们就可以在项目根目录下执行 make、make all 或者 make build 来编译出 seckill。接下来,我们执行 ./seckill help 命令便可以看到 seckill 的帮助信息。如下所示:

A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
  seckill [command]
Available Commands:
  admin       A brief description of your command
  api         A brief description of your command
  bench       A brief description of your command
  help        Help about any command
Flags:
  -c, --config string   config file (default is $HOME/.seckill.yaml)
  -h, --help            help for seckill
  -t, --toggle          Help message for toggle
Use "seckill [command] --help" for more information about a command.

Drawing 2.png

小结

这一讲,我根据开源项目规范和 DDD ,介绍了如何设计秒杀项目的目录结构,以及强大的命令行工具 cobra ,希望你学习后能在工作中运用自如,开发出代码目录结构清晰、命令行交互体验良好的程序。

另外,除了目录结构规范和命令行参数规范,Go 也有自己的代码规范,你可以通过学习 《Effective Go》来熟悉。

前面我只介绍了业务系统类的项目规范,你也可以思考下:一个公共库项目或者命令行工具,它的目录结构应该是什么样的?

你可以通过观察多个优秀的开源项目找到答案,然后写在留言区哦。

好了,这一讲就到这里了,下一讲我将给你介绍“如何解决程序升级中的稳定性问题”。到时见!

源码地址:https://github.com/lagoueduCol/MiaoSha-Yiletian


精选评论

**0835:

请问这是什么问题呢?

    讲师回复:

    项目规范,如何组织代码目录结构的问题。

本文含有隐藏内容,请 开通VIP 后查看