现在来看一下如何组织我们的代码。
包
为了组织复杂的库和系统代码,我们需要学习关于包的知识。在 Go 语言中,包名遵循 Go 项目的目录结构。如果我们建立一个购物系统,我们可能以 “shopping” 包名作为一个开始,然后把所有源代码文件放到 $GOPATH/src/shopping/
目录中。
我们不会去想把所有东西都放在这个文件夹中。例如,我们可能想单独把数据库逻辑放在它自己的目录中。为了实现这个,我们创建一个子目录 $GOPATH/src/shopping/db
。子目录中文件的包名就是 db
,但是为了从另一个包访问它,包括 shopping
包,我们需要导入 shopping/db
。
换句话说,当你想去命名一个包的时候,可以通过 package
关键字,提供一个值,而不是完整的层次结构(例如:「shopping」或者 「db」)。当你想去导入一个包的时候,你需要指定完整路径。
接下来,我们去尝试下。在你的 Go 的工作目录 src
文件夹下(我们已经在 基础 那一章节中介绍了),创建一个新的文件夹叫做 shopping
,然后在 shopping
文件夹下创建一个 db
文件夹。
在 shopping/db
文件夹下,创建一个叫做 db.go
的文件,然后在 db.go
文件中添加如下的代码:
package db
type Item struct {
Price float64
}
func LoadItem(id int) *Item {
return &Item{
Price: 9.001,
}
}
需要注意包名和文件夹名是一样的。而且很明显我们实际并没有连接数据库。这里使用这个例子只是为了展示如何组织代码。
现在,在主目录 shopping
下创建一个叫 pricecheck.go
的文件。它的内容是:
package shopping
import (
"shopping/db"
)
func PriceCheck(itemId int) (float64, bool) {
item := db.LoadItem(itemId)
if item == nil {
return 0, false
}
return item.Price, true
}
很有可能认为导入 shopping/db
有点特别,因为我们已经在 shopping
包/目录中。实际上,我们正在导入 $GOPATH/src/shopping/db
,这意味着只要你在你的工作区间 src/test
目录中有一个名为 db
的包,你就可以轻松导入它。
你正在构建一个包,除了我们看到的你不再需要任何东西。为了构建一个可执行程序,你仍然需要 main
包。我比较喜欢的方式是在 shopping
目录下创建一个 main
子目录,然后再创建一个叫 main.go
的文件,下面是它的内容:
package main
import (
"shopping"
"fmt"
)
func main() {
fmt.Println(shopping.PriceCheck(4343))
}
现在,你可以进入你的 shopping
项目运行代码,输入:
go run main/main.go
循环导入
当你编写更复杂的系统的时,你必然会遇到循环导入。例如,当 A
包导入 B
包,B
包又导入 A
包(间接或者直接导入)。这是编译器不能允许的。
让我们改变我们的 shopping
结构以复现这个错误。
将 Item
定义从 shopping/db/db.go
移到 shopping/pricecheck.go
。你的 pricecheck.go
文件像下面这样:
package shopping
import (
"shopping/db"
)
type Item struct {
Price float64
}
func PriceCheck(itemId int) (float64, bool) {
item := db.LoadItem(itemId)
if item == nil {
return 0, false
}
return item.Price, true
}
如果你尝试运行代码,你会从 db/db.go
得到两个关于 Item
未定义的错误。这看起来是说 Item
不存在 db
包中。它已经被移动到 shopping
包中,我们需要将 shopping/db/db.go
改变成:
package db
import (
"shopping"
)
func LoadItem(id int) *shopping.Item {
return &shopping.Item{
Price: 9.001,
}
}
现在但你尝试运行代码的时候,你将会得到 不允许循环导入 的错误。我们可以通过引入另一个包含共享结构体的包来解决这个问题。你的目录现在看起来像这个样子:
$GOPATH/src
- shopping
pricecheck.go
- db
db.go
- models
item.go
- main
main.go
pricecheck.go
将仍然导入 shopping/db
,但是 db.go
现在导入 shopping/models
而不是 shopping
,因此打破了循环。因为我们将共享的 Item
结构体移动到 shopping/models/item.go
,我们现在需要去改变 shopping/db/db.go
从 models
包中引用 Item
结构体。
package db
import (
"shopping/models"
)
func LoadItem(id int) *models.Item {
return &models.Item{
Price: 9.001,
}
}
你经常需要共享某些代码,不止 models
,所以你可能有其他类似叫做 utilities
的目录,这些共享包的重要原则是它们不从 shopping
包或者任何子包中导入任何东西。在后面的章节中,我们将介绍可以帮助我们解决这些类型依赖关系的接口。
可见性
Go 用了一个简单的规则去定义什么类型和函数可以包外可见。如果类型或者函数名称以一个大写字母开始,它就具有了包外可见性。如果以一个小写字母开始,它就不可以。
这也可以应用到结构体字段。如果一个字段名以一个小写字母开始,只有包内的代码可以访问它们。
例如,我们的 items.go
文件中有个这样的函数:
func NewItem() *Item {
// ...
}
它可以通过 models.NewItem()
这样被调用。但是如果函数命名为 newItem
,我们将不能从不同的包访问它了。
去试试更改 shopping
代码中的函数,类型以及字段的名称。例如,如果你将 Item
的 Price
字段命名为 price
,你应该会获得一个错误。
包管理
我们用来 build
和 run
的 go
命令有一个 get
子命令,用于获取第三方库。go get
支持除了这个例子中的各种协议,我们可以从 Github 中获取一个库,意味着,你需要在你的电脑中安装 git
。
假设你已经安装了 Git,在 shell 中输入命令:
go get github.com/mattn/go-sqlite3
go get
获取远端的文件并把它们存储在你的工作区间中。去看看你的 $GOPATH/src
目录,你会发现除了我们创建的 shopping
项目之外,还有一个 github.com
目录,在里面,你会看到一个包含了 go-sqlite3
目录的 mattn
目录。
我们刚才只是讨论了如何导入我们工作区间的包。为了导入新安装的 go-sqlite3
包,我们要这样导入:
import (
"github.com/mattn/go-sqlite3"
)
我知道这看起来像一个 URL,实际上,它只是希望导入在 $GOPATH/src/github.com/mattn/go-sqlite3
找到的 go-sqlite3
包。
依赖管理
go get
还有一些其他的技巧。如果我们在一个项目内使用 go get
,它将浏览所有文件,查找 imports
的第三方库然后下载他们。某种程度上,我们的源代码变成了 Gemfile
或者 package.json
。
如果你调用 go get -u
,它将更新所有包(或者你可以通过 go get -u FULL_PACKAGE_NAME
更新一个具体的包)。
最后,你可能发现了 go get
的不足。一方面,这儿没有办法指定一个版本。他总是指向 master/head/trunk/default
。这是一个较大的问题如果你有两个项目需要同一个库的不同版本。
为了解决这个问题,你可以使用一个第三方的依赖管理工具。他们仍然很年轻,但 goop 和 godep 是可信的。更多完整的列表在 go-wiki。