1. 目录规范
一个好的目录结构至少要满足以下几个要求。
- 命名清晰:目录命名要清晰、简洁,不要太长,也不要太短,目录名要能清晰地表达出该目录实现的功能,并且目录名可根据实际情况选择单数或者复数。
- 功能明确:一个目录所要实现的功能应该是明确的、并且在整个项目目录中具有很高的辨识度。也就是说,当需要新增一个功能时,我们能够非常清楚地知道把这个功能放在哪个目录下。
- 全面性:目录结构应该尽可能全面地包含研发过程中需要的功能,例如文档、脚本、源码管理、API 实现、工具、第三方包、测试、编译产物等。
- 可预测性:项目规模一定是从小到大的,所以一个好的目录结构应该能够在项目变大时,仍然保持之前的目录结构。
- 可扩展性:每个目录下存放了同类的功能,在项目变大时,这些目录应该可以存放更多同类功能。
根据功能,我们可以将目录结构分为结构化目录结构和平铺式目录结构两种。
- 结构化目录结构主要用在
Go
应用中,相对来说比较复杂; - 而平铺式目录结构主要用在
Go
包中,相对来说比较简单;
2. 平铺式目录结构
一个 Go
项目可以是一个应用,也可以是一个代码框架 / 库,当项目是代码框架 / 库时,比较适合采用平铺式目录结构。
平铺方式就是在项目的根目录下存放项目的代码,整个目录结构看起来更像是一层的,例如 log
包 github.com/golang/glog
就是平铺式的,目录如下:
$ ls glog/
glog_file.go glog.go glog_test.go LICENSE README
3. 结构化目录结构
当前 Go
社区比较推荐的结构化目录结构是 https://github.com/golang-standards/project-layout。虽然它并不是官方和社区的规范,但因为组织方式比较合理,被很多 Go
开发人员接受。
├── api
├── assets
├── build
├── cmd
├── configs
├── deployments
├── docs
├── examples
├── githooks
├── go.mod
├── init
├── internal
├── LICENSE.md
├── Makefile
├── pkg
├── README_zh-CN.md
├── scripts
├── test
├── third_party
├── tools
├── vendor
├── web
└── website
一个 Go
项目包含 3 大部分:Go
应用 、项目管理和文档。所以,我们的项目目录也可以分为这 3 大类。同时,Go
应用又贯穿开发阶段、测试阶段和部署阶段,相应的应用类的目录,又可以按开发流程分为更小的子类。所以整体来看,我们的目录结构可以按下图所示的方式来分类:
3.1 Go 应用开发目录
开发的代码包含前端代码和后端代码,可以分别存放在前端目录和后端目录中。
3.1.1 /web
前端代码存放目录,主要用来存放 Web
静态资源,服务端模板和单页应用(SPAs
)。
3.1.2 /cmd
一个项目有很多组件,可以把组件 main
函数所在的文件夹统一放在 /cmd
目录下,例如:
$ ls cmd/
gendocs geniamdocs genman genyaml apiserver iamctl iam-pump
$ ls cmd/apiserver/
apiserver.go
这里要保证 /cmd/<组件名>
目录下不要存放太多的代码,如果你认为代码可以导入并在其他项目中使用,那么它应该位于 /pkg
目录中。如果代码不是可重用的,或者你不希望其他人重用它,请将该代码放到 /internal
目录中。
3.1.3 /internal
存放私有应用和库代码。如果一些代码,你不希望在其他应用和库中被导入,可以将这部分代码放在/internal
目录下。
在引入其它项目 internal
下的包时,Go
语言会在编译时报错:
An import of a path containing the element “internal” is disallowed
if the importing code is outside the tree rooted at the parent of the
"internal" directory.
如果 internal
目录下直接存放每个组件的源码目录(一个项目可以由一个或多个组件组成),当项目变大、组件增多时,可以将新增加的组件代码存放到 internal
目录,这时 internal
目录就是可扩展的。例如:
$ ls internal/
apiserver authzserver iamctl pkg pump watcher
/internal
目录建议包含如下目录:
-
/internal/apiserver
:该目录中存放真实的应用代码。这些应用的共享代码存放在/internal/pkg
目录下。 -
/internal/pkg
:存放项目内可共享,项目外不共享的包。这些包提供了比较基础、通用的功能,例如工具、错误码、用户验证等功能。
建议是,一开始将所有的共享代码存放在 /internal/pkg
目录下,当该共享代码做好了对外开发的准备后,再转存到 /pkg
目录下。
3.1.4 /pkg
该目录中存放可以被外部应用使用的代码库,其他项目可以直接通过 import
导入这里的代码。所以,我们在将代码库放入该目录时一定要慎重。
3.1.5 /vendor
项目依赖,可通过 go mod vendor
创建。需要注意的是,如果是一个 Go
库,不要提交 vendor
依赖包。
3.1.6 /third_party
外部帮助工具,分支代码或其他第三方应用(例如 Swagger UI)。比如我们 fork
了一个第三方 go
包,并做了一些小的改动,我们可以放在目录 /third_party/forked
下。一方面可以很清楚的知道该包是 fork
第三方的,另一方面又能够方便地和 upstream
同步。
3.2 Go 应用测试目录
3.2.1 /test
用于存放其他外部测试应用和测试数据。/test
目录的构建方式比较灵活:对于大的项目,有一个数据子目录是有意义的。例如,如果需要 Go
忽略该目录中的内容,可以使用 /test/data
或 /test/testdata
目录。
需要注意的是,Go
也会忽略以 .
或 _
开头的目录或文件。这样在命名测试数据目录方面,可以具有更大的灵活性。
3.3 Go 应用部署目录
3.3.1 /configs
这个目录用来配置文件模板或默认配置。例如,可以在这里存放 confd
或 consul-template
模板文件。这里有一点要注意,配置中不能携带敏感信息,这些敏感信息,我们可以用占位符来替代,例如:
apiVersion: v1
user:
username: ${CONfig_USER_USERNAME} # iam 用户名
password: ${CONfig_USER_PASSWORD} # iam 密码
3.3.2 /deployments
用来存放 Iaas
、PaaS
系统和容器编排部署配置和模板(Docker-Compose
,Kubernetes/Helm
,Mesos
,terraform
,Bosh
)。在一些项目,特别是用 Kubernetes
部署的项目中,这个目录可能命名为 deploy
。
3.3.3 /init
存放初始化系统(systemd
,upstart
,sysv
)和进程管理配置文件(runit
,supervisord
)。比如 sysemd
的 unit
文件。这类文件,在非容器化部署的项目中会用到。
3.4 Go 应用项目管理目录
3.4.1 /Makefile
一个 Go
项目在其根目录下应该有一个 Makefile
工具,用来对项目进行管理,Makefile
通常用来执行静态代码检查、单元测试、编译等功能。其他常见功能:
- 静态代码检查(
lint
):推荐用golangci-lint
。 - 单元测试(
test
):运行go test ./...
。 - 编译(
build
):编译源码,支持不同的平台,不同的cpu
架构。 - 镜像打包和发布(
image/image.push
):现在的系统比较推荐用Docker/Kubernetes
进行部署,所以一般也要有镜像构建功能。 - 清理(
clean
):清理临时文件或者编译后的产物。 - 代码生成(
gen
):比如要编译生成protobuf pb.go
文件。 - 部署(
deploy
,可选):一键部署功能,方便测试。 - 发布(
release
):发布功能,比如:发布到Docker Hub
、github
等。 - 帮助(
help
):告诉Makefile
有哪些功能,如何执行这些功能。 - 版权声明(
add-copyright
):如果是开源项目,可能需要在每个文件中添加版权头,这可以通过Makefile
来添加。 - API 文档(
swagger
):如果使用swagger
来生成API
文档,这可以通过Makefile
来生成。
建议:直接执行 make
时,执行如下各项 format -> lint -> test -> build
,如果是有代码生成的操作,还可能需要首先生成代码 gen -> format -> lint -> test -> build
。
3.4.2 /scripts
该目录主要用来存放脚本文件,实现构建、安装、分析等不同功能。不同项目,里面可能存放不同的文件,但通常可以考虑包含以下 3 个目录:
-
/scripts/make-rules
:用来存放makefile
文件,实现/Makefile
文件中的各个功能。Makefile
有很多功能,为了保持它的简洁,我建议你将各个功能的具体实现放在/scripts/make-rules
文件夹下 -
/scripts/lib
:shell
库,用来存放shell
脚本。一个大型项目中有很多自动化任务,比如发布、更新文档、生成代码等,所以要写很多shell
脚本,这些shell
脚本会有一些通用功能,可以抽象成库,存放在/scripts/lib
目录下,比如logging.sh
,util.sh
等。 -
/scripts/install
:如果项目支持自动化部署,可以将自动化部署脚本放在此目录下。如果部署脚本简单,也可以直接放在/scripts
目录下。
另外,shell
脚本中的函数名,建议采用语义化的命名方式,例如 iam::log::info
这种语义化的命名方式,可以使调用者轻松的辨别出函数的功能类别,便于函数的管理和引用。
3.4.3 /build
这里存放安装包和持续集成相关的文件。这个目录下有 3 个大概率会使用到的目录,在设计目录结构时可以考虑进去。
-
/build/package
:存放容器(Docker
)、系统(deb
,rpm
,pkg
)的包配置和脚本。 -
/build/ci
:存放CI
的配置文件和脚本。 -
/build/docker
:存放子项目各个组件的Dockerfile
文件。
3.4.4 /tools
存放这个项目的支持工具。这些工具可导入来自 /pkg
和 /internal
目录的代码。
3.4.5 /githooks
Git
钩子。比如,我们可以将 commit-msg
存放在该目录。
3.4.6 /assets
项目使用的其他资源 (图片、CSS
、JavaScript
等)。
3.4.7 /website
如果你不使用 Github
页面,则在这里放置项目的网站数据。
3.5 Go 应用文档目录
3.5.1 /README.md
项目的 README
文件一般包含了项目的介绍、功能、快速安装和使用指引、详细的文档链接以及开发指引等。
3.5.2 /docs
存放设计文档、开发文档和用户文档等(除了 godoc
生成的文档)。推荐存放以下几个子目录:
-
/docs/devel/{en-US,zh-CN}
:存放开发文档、hack
文档等。 -
/docs/guide/{en-US,zh-CN}
: 存放用户手册,安装、quickstart
、产品文档等,分为中文文档和英文文档。 -
/docs/images
:存放图片文件。
3.5.3 /CONTRIBUTING.md
开源就绪的项目,用来说明如何贡献代码,如何开源协同等等。CONTRIBUTING.md
不仅能够规范协同流程,还能降低第三方开发者贡献代码的难度。
3.5.4 /api
/api
目录中存放的是当前项目对外提供的各种不同类型的 API
接口定义文件,其中可能包含类似 /api/protobuf-spec
、/api/thrift-spec
、/api/http-spec
、openapi
、swagger
的目录,这些目录包含了当前项目对外提供和依赖的所有 API
文件。
3.5.5 /LICENSE
版权文件可以是私有的,也可以是开源的。常用的开源协议有:Apache 2.0
、MIT
、BSD
、GPL
、Mozilla
、LGPL
。
3.5.6 /CHANGELOG
当项目有更新时,为了方便了解当前版本的更新内容或者历史更新内容,需要将更新记录存放到 CHANGELOG
目录。
3.5.7 /examples
存放应用程序或者公共包的示例代码
4. 不建议的目录
4.1 /src
其中一个重要的原因是:在默认情况下,Go
语言的项目都会被放置到 $GOPATH/src
目录下。这个目录中存放着所有代码,如果我们在自己的项目中使用 /src
目录,这个包的导入路径中就会出现两个 src
,例如:
$GOPATH/src/github.com/marmotedu/project/src/main.go
这样的目录结构看起来非常怪。
5. 建议
对于小型项目,可以考虑先包含 cmd
、pkg
、internal
3 个目录,其他目录后面按需创建,例如:
$ tree --noreport -L 2 tms
tms
├── cmd
├── internal
├── pkg
└── README.md
另外,在设计目录结构时,一些空目录无法提交到 Git
仓库中,但我们又想将这个空目录上传到 Git
仓库中,以保留目录结构。这时候,可以在空目录下加一个 .keep
文件,例如:
$ ls -A build/ci/
.keep
6. 实际项目参考目录
├── admin.sh # 进程的start|stop|status|restart控制文件
├── conf # 配置文件统一存放目录
│ ├── config.yaml # 配置文件
│ ├── server.crt # TLS配置文件
│ └── server.key
├── config # 专门用来处理配置和配置文件的Go package
│ └── config.go
├── db.sql # 在部署新环境时,可以登录MysqL客户端,执行source db.sql创建数据库和表
├── docs # swagger文档,执行 swag init 生成的
│ ├── docs.go
│ └── swagger
│ ├── swagger.json
│ └── swagger.yaml
├── handler # 类似MVC架构中的C,用来读取输入,并将处理流程转发给实际的处理函数,最后返回结果
│ ├── handler.go
│ ├── sd # 健康检查handler
│ │ └── check.go
│ └── user # 核心:用户业务逻辑handler
│ ├── create.go # 新增用户
│ ├── delete.go # 删除用户
│ ├── get.go # 获取指定的用户信息
│ ├── list.go # 查询用户列表
│ ├── login.go # 用户登录
│ ├── update.go # 更新用户
│ └── user.go # 存放用户handler公用的函数、结构体等
├── main.go # Go程序唯一入口
├── Makefile # Makefile文件,一般大型软件系统都是采用make来作为编译工具
├── model # 数据库相关的操作统一放在这里,包括数据库初始化和对表的增删改查
│ ├── init.go # 初始化和连接数据库
│ ├── model.go # 存放一些公用的go struct
│ └── user.go # 用户相关的数据库CURD操作
├── pkg # 引用的包
│ ├── auth # 认证包
│ │ └── auth.go
│ ├── constvar # 常量统一存放位置
│ │ └── constvar.go
│ ├── errno # 错误码存放位置
│ │ ├── code.go
│ │ └── errno.go
│ ├── token
│ │ └── token.go
│ └── version # 版本包
│ ├── base.go
│ ├── doc.go
│ └── version.go
├── README.md # API目录README
├── router # 路由相关处理
│ ├── middleware # API服务器用的是Gin Web框架,Gin中间件存放位置
│ │ ├── auth.go
│ │ ├── header.go
│ │ ├── logging.go
│ │ └── requestid.go
│ └── router.go
├── service # 实际业务处理函数存放位置
│ └── service.go
├── util # 工具类函数存放目录
│ ├── util.go
│ └── util_test.go
└── vendor # vendor目录用来管理依赖包
├── github.com
├── golang.org
├── gopkg.in
└── vendor.json
Go API
项目中,一般都会包括这些功能项:Makefile
文件、配置文件目录、RESTful API
服务器的 handler
目录、model
目录、工具类目录、vendor
目录,以及实际处理业务逻辑函数所存放的 service
目录。这些都在上述的代码结构中有列出,新加功能时将代码放入对应功能的目录/文件中,可以使整个项目代码结构更加清晰,非常有利于后期的查找和维护。
参考:
https://juejin.cn/book/6844733730678898702/section/6844733730720841735
7. makefile 的规则
Makefile
基本格式如下:
target ... : prerequisites ...
command
...
其中:
-
target
:编译文件要生成的目标 -
prerequisites
:编译文件需要的依赖 -
command
:依赖生成目标所需要执行的命令(任意的shell
命令),Makefile
中的命令必须以[tab]
开头
比如我们平时使用的 gcc a.c b.c -o test
这里的 test 就是我们要生成的目标, a.c
、b.c
就是我们生成目标需要的依赖,而 gcc a.c b.c -o test
则是命令。将这行命令用 Makefile
的方式来写就是:
test: a.c b.c
gcc a.c b.c -o test
all: gotool
@go build -v .
clean:
rm -f apiserver
find . -name "[._]*.s[a-w][a-z]" | xargs -i rm -f {}
gotool:
gofmt -w .
go tool vet . |& grep -v vendor;true
ca:
openssl req -new -nodes -x509 -out conf/server.crt -keyout conf/server.key -days 3650 -subj "/C=DE/ST=NRW/L=Earth/O=Random Company/OU=IT/CN=127.0.0.1/emailAddress=xxxxx@qq.com"
help:
@echo "make - compile the source code"
@echo "make clean - remove binary file and vim swp files"
@echo "make gotool - run go tool 'fmt' and 'vet'"
@echo "make ca - generate ca files"
.PHONY: clean gotool ca help
上面的 Makefile
文件中,.PHONY
是个伪目标,形式上是一个目标,但是不需要依赖,伪目标一般只是为了执行目标下面的命令(比如 clean
就是伪目标)。@
放在行首,表示不打印此行。默认在编译的过程中,会把此行的展开效果字符串打印出来。
上面的 Makefile
实现了如下功能:
-
make
:执行go build -v .
生成Go
二进制文件 -
make gotool
:执行gofmt -w .
和go tool vet .
(格式化代码和源码静态检查) -
make clean
:做一些清理工作:删除二进制文件、删除vim swp
文件 -
make ca
:生成证书 -
make help
:打印 help 信息
package main
import (
"encoding/json"
"fmt"
"os"
"runtime"
"github.com/spf13/pflag"
)
var (
version = pflag.BoolP("version", "v", false, "show version info.")
)
var (
gitTag string = ""
gitCommit string = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD)
gitTreeState string = "not a git tree" // state of git tree, either "clean" or "dirty"
buildDate string = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%sZ')
)
// Info contains versioning information.
type Info struct {
GitTag string `json:"gitTag"`
GitCommit string `json:"gitCommit"`
GitTreeState string `json:"gitTreeState"`
BuildDate string `json:"buildDate"`
GoVersion string `json:"goVersion"`
Compiler string `json:"compiler"`
Platform string `json:"platform"`
}
// String returns info as a human-friendly version string.
func (info Info) String() string {
return info.GitTag
}
func Get() Info {
return Info{
GitTag: gitTag,
GitCommit: gitCommit,
GitTreeState: gitTreeState,
BuildDate: buildDate,
GoVersion: runtime.Version(),
Compiler: runtime.Compiler,
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
}
}
func main() {
pflag.Parse()
if *version {
v := Get()
marshalled, err := json.MarshalIndent(&v, "", " ")
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
}
fmt.Println(string(marshalled))
return
}
}
SHELL := /bin/bash
BASEDIR = $(shell pwd)
# build with verison infos
versionDir = "apiserver/pkg/version"
gitTag = $(shell if [ "`git describe --tags --abbrev=0 2>/dev/null`" != "" ];then git describe --tags --abbrev=0; else git log --pretty=format:'%h' -n 1; fi)
buildDate = $(shell TZ=Asia/Shanghai date +%FT%T%z)
gitCommit = $(shell git log --pretty=format:'%H' -n 1)
gitTreeState = $(shell if git status|grep -q 'clean';then echo clean; else echo dirty; fi)
ldflags="-w -X ${versionDir}.gitTag=${gitTag} -X ${versionDir}.buildDate=${buildDate} -X ${versionDir}.gitCommit=${gitCommit} -X ${versionDir}.gitTreeState=${gitTreeState}"
all: gotool
@go build -v -ldflags ${ldflags} .
clean:
rm -f apiserver
find . -name "[._]*.s[a-w][a-z]" | xargs -i rm -f {}
gotool:
gofmt -w .
go tool vet . |& grep -v vendor;true
ca:
openssl req -new -nodes -x509 -out conf/server.crt -keyout conf/server.key -days 3650 -subj "/C=DE/ST=NRW/L=Earth/O=Random Company/OU=IT/CN=127.0.0.1/emailAddress=xxxxx@qq.com"
help:
@echo "make - compile the source code"
@echo "make clean - remove binary file and vim swp files"
@echo "make gotool - run go tool 'fmt' and 'vet'"
@echo "make ca - generate ca files"
.PHONY: clean gotool ca help
其中 gitTag
、gitCommit
、gitTreeState
等变量的值是通过 -ldflags -X importpath.name=value
在编译时传到程序中的。为此我们需要在编译时传入这些信息,并在 go build
中添加这些 flag
。
go build -v -ldflags ${ldflags} .
-w
为去掉调试信息(无法使用 gdb
调试),这样可以使编译后的二进制文件更小。
$ ./apiserver -v
{
"gitTag": "7322949",
"gitCommit": "732294928b3c4dff5b898fde0bb5313752e1173e",
"gitTreeState": "dirty",
"buildDate": "2018-06-05T07:43:26+0800",
"goVersion": "go1.10.2",
"compiler": "gc",
"platform": "linux/amd64"
}
我们可以将这些信息写在配置文件中,程序运行时从配置文件中取得这些信息进行显示。但是在部署程序时,除了二进制文件还需要额外的配置文件,不是很方便。或者将这些信息写入代码中,这样不需要额外的配置,但要在每次编译时修改代码文件,也比较麻烦。Go 官方提供了一种更好的方式:通过 -ldflags -X importpath.name=value
(详见 -ldflags -X importpath.name=value
)来给程序自动添加版本信息。https://golang.org/cmd/link/
package main
import "fmt"
var (
VERSION string
BUILD_TIME string
GO_VERSION string
)
func main() {
fmt.Printf("%s\n%s\n%s\n", VERSION, BUILD_TIME, GO_VERSION)
}
编译命令
go build -ldflags "-w -s -X main.VERSION=1.0.0 -X 'main.BUILD_TIME=`date`' -X 'main.GO_VERSION=`go version`'"
因为
date
和go version
的输出有空格,所以main.BUILD_TIME
和main.GO_VERSION
必须使用引号括起来
-
-w
去掉DWARF
调试信息,得到的程序就不能用gdb
调试了。 -
-s
去掉符号表,panic
时候的stack trace
就没有任何文件名/行号信息了,这个等价于普通C/C++
程序被strip
的效果,-w -s
如果使用这两个将会看不见文件名、行号, 对于调试不利gdb
看不到源码 -
-X
设置包中的变量值
gcflags
go
在编译目标程序的时候会嵌入运行时( runtime
)的二进制,禁止优化和内联可以让运行时(runtime
)中的函数变得更容易调试。
go build -gcflags='all=-N -l' main.go
原文地址:https://www.jb51.cc/wenti/3280541.html
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。