Golang 项目布局浅析
康凯 360云计算
女主宣言
Golang作为当下云开发中最为流行的语言之一,越来越受到广大程序员的青睐。开发Golang项目经常遇到的一个常见问题是如何组织项目结构布局。今天作者从项目结构以及对内部、外部包的引用来讲讲布局问题,希望对大家有所帮助。
PS:丰富的一线技术、多元化的表现形式,尽在“360云计算”,点关注哦!
开始编码之前,我们需要先明确一些问题:
- 项目结构如何反映代码的引入方式?
- 除代码外,如何组织项目的命令行工具?
- 如何灵活的在不同模块间组织项目代码?
- 多个包如何在一个模块中共存?
我们先明确一些名词和概念:
Internal packages,内部私有包,只能从其模块中的其他包引入,不能被外部包引入。用户可以使用 go get 安装需要的外部包。
1
Helloworld
我们打开一个项目,项目路径是模块名。项目的 go.mod 文件包含以下行:
module github.com/qidian/modlib
Go项目通常通过其GitHub路径命名。Go还支持自定义名称,本文暂不赘述,之后开新帖再说。我们可以暂时用github.com/your-handle/your-project
或your-project-domain.io
替换github.com/qidian/modlib
。
模块名称非常重要,因为他是项目代码中引入包名的基础:
2
项目布局
先来看一个项目 modlib 的目录和文件布局:
├── LICENSE
├── README.md
├── config.go
├── go.mod
├── go.sum
├── clientlib
│ ├── lib.go
│ └── lib_test.go
├── cmd
│ ├── modlib-client
│ │ └── main.go
│ └── modlib-server
│ └── main.go
├── internal
│ └── auth
│ ├── auth.go
│ └── auth_test.go
└── serverlib
└── lib.go
让我们从根目录中的文件开始。
go.mod 是模块定义文件。它包含上面显示的模块名称,我的项目没有依赖项。依赖项与项目布局设计无关。有需要的同学可以从 Golang 官方博客学习。
go.sum 由 go tools 管理,其包含所有依赖项校验值。
config.go,这是我们查看的第一个代码文件,它包含一个简单的 Config()函数:
package modlib
func Config() string {
return "modlib config"
}
第一行申明包名,由于该文件位于模块的顶层,因此其程序包名称即为模块名称。
关于如何引入 github.com/qidian/modlib ,我们来看一个例子:
package main
import "fmt"
import "github.com/qidian/modlib"
func main() {
fmt.Println(modlib.Config())
}
因此,如果您的模块提供单个 package,或者您要从模块的顶层软件包中导出代码,则将其所有代码放在模块的顶层目录中,并命名该 package 作为模块路径的最后一部分(除非您使用更灵活的 vanity imports)。
3
引用外部包
clientlib / lib.go 是我们对 clientlib 模块封装的文件。文件名可以根据业务逻辑来命名:
package clientlib
func Hello() string {
return "clientlib hello"
}
我们再来看下面这个例子,通过 github.com/qidian/modlib/clientlib 导入 clientlib:
package main
import "fmt"
import "github.com/qidian/modlib"
import "github.com/qidian
/modlib/clientlib"
func main() {
fmt.Println(modlib.Config())
fmt.Println(clientlib.Hello())
}
serverlib目录包含了另一个用户可以引入的package。这里展示了多个程序包如何在代码结构中并存。
关于包嵌套,它可以根据需要增加目录层级。我们可见的 package 名称由模块根目录的相对路径确定。例如,如果我们有一个 clientlib/tokens
的子目录 ,并在tokens包中包含一些代码,则用户将使用如下代码引入该目录。
import "github.com/qidian/modlib/clientlib/tokens“
对于一些模块而言,一个顶级 package 就足够业务开发了。在本例中没有用户可导入的 package 子目录,但是所有代码都在 modlib 的单个或多个 Go 文件中。
4
Commands
一些Go项目还需要制作可执行程序,我们一般会再增加一个cmd目录。
该目录是项目所有命令行程序的常规位置。程序的命名方案通常为:
用户可以使用go工具按如下方式安装此类命令:
$ go get github.com/qidian/modlib/cmd/cmd-name
# Go downloads, builds and installs cmd-name into the default location.
# The bin/ directory in the default location is often in $PATH, so we can
# just invoke cmd-name now
$ cmd-name ...
在modlib中,提供了两个不同的命令行程序作为示例:modlib-client和modlib-server。在每个代码中,代码都在包main中;文件名为main.go。
这是我们在测试环境上运行的命令:
$ go get github.com/qidian/modlib/cmd/modlib-client
$ modlib-client
Running client
Config: modlib config
clientlib hello
$ go get github.com/qidian/modlib/cmd/modlib-server
$ modlib-server
Running server
Config: modlib config
Auth: thou art authorized
serverlib hello
# Clean up...
$ rm -f `which modlib-server` `which modlib-client`
我们来看看 modlib-serve.go 是如何从 modlib 中导入其他代码的:
package main
import (
"fmt"
"github.com/qidian/modlib"
"github.com/qidian/modlib/internal/auth"
"github.com/qidian/modlib/serverlib"
)
func main() {
fmt.Println("Running server")
fmt.Println("Config:", modlib.Config())
fmt.Println("Auth:", auth.GetAuth())
fmt.Println(serverlib.Hello())
}
Golang 里的绝对导入适用于引入package和二进制命令,这是 clientlib 中的代码需要引入 modlib 时的例子:
github.com/eliben/modlib
5
私有包
另一个重要概念是私有包,也就是项目内部使用的软件包,并且我们不想导出给外部用户。由于语义版本控制,这在Go模块中尤其重要。您的项目在v1中导出的所有内容都将成为公共API,并且必须遵守语义版本兼容性。
Go工具将内部包识别为特殊路径,只有同一模块中的软件包可以引入它。如果我们尝试在外部模块代码中引用,则会抛错:
use of internal package github.com/eliben/modlib/internal/auth not allowed
在本文样例中,internal中只有一个package。而在实际的工程项目里通常会有一堆完整的package目录树。
将内部API重构并将其导出给其他同学很容易,但是使用外部API并取消导出会很麻烦。所以我在开发时会尽可能地将模块需要的私有包和代码放入内部包中。
最后再举个例子,如果一个网站项目 repo 中,我们将代码安排在 internal/website中。用于项目的内部工具和脚本也是如此。这样一来项目的根目录是最清晰并且对开发者来说更友好。理想情况下,开发者通过项目代码布局就可以大致了解他们想了解的东西所在位置,因此将一些代码放在内部会很有意义。
有疑问加站长微信联系(非本文作者)