go包


go包

为了理解更复杂的库和组织系统,我们需要学习包。在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,并写入下面的代码:

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包,这也可以通过test/db导入这个db包。

如果你打算构建一个包,你只需要做以上的步骤。要构建一个可执行文件,你还得需要一个main。我最喜欢的是在shopping目录创建一个子目录main,并创建一个main.go文件,写入以下代码:

package main

import (
    "fmt"
    "shopping"
)

func main() {
    fmt.Println(shopping.PriceCheck(4343))
}

现在你可以运行进入你的shopping项目,输入以下命令运行你的代码:

go run main/main.go

4.1.1 循环导入

当你开始写更复杂的系统时,你一定会遇到循环导入。当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未定义的错误。这是有意义的。db包不再存在Item;它已经被移动到shopping包。我们需要去改变shopping/db/db.go为:

package db

import (
    "shopping"
)

func LoadItem(id int) *shopping.Item {
    return &shopping.Item{
        Price: 9.001,
    }
}

现在再运行一下代码,你会得到一个严重错误:import cycle not allowed,不允许循环导入。要解决这个问题,需要导入另外一个包,这个包定义了共享结构体。你的目录结构应该像下面这样:

$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或者子包中导入任何东西。在一些小节,我们会看到通过接口可以帮助我们清理这些类型的依赖关系。

4.1.2 可见性

go语言使用了一种简单的规则来规定类型或者函数是否对外部的包可见。如果你命名类型或者函数时以一个大写字母开头,那么这个类型和函数就是可见的。如果使用一个小写字母开头,那么就是不可见的。

结构体的字段也使用相同的方式。如果一个结构体的字段名是以小写字母开头的,那么只有在同一个包中的代码才能访问这些字段。

例如,在我们的items.go文件中有一个函数类似这样:

func NewItem() *Item {
    // ...
}

可以通过models.NewItem()调用这个函数。但是如果这个函数名为newItem,那么我们从其他的包是不能调用这个函数的。

继续改变shopping包中函数名、类型名和字段名。例如,如果你将结构体ItemPrice字段改成price,你会得到一个错误。

4.1.3 包管理

我们已经使用过go语言的命令如go rungo build,go还有一个子命令get可以用来获取第三方库的代码。go get支持很多种协议,但是这个例子中,我们将通过它从github上得到一个库,这意味着你必须在你的电脑上安装git。

加入你已经安装了git,在命令行中输入:

go get github.com/mattn/go-sqlite3

go get将得到这些远程文件并将它们保存在你的工作空间。现在去查看你的$GOPATH/src。除了我们已经创建的shopping工程,你会看见一个github.com文件夹。明确在文件夹中你会看见一个mattn文件夹,它包含了一个go-sqlite3文件夹。

我们已经学习了如何导入一个包到我们的工作空间。现在如果想使用我们刚刚获取的go-sqlite3包,可以通过以下方式导入:

import (
    "github.com/mattn/go-sqlite3"
)

我知道这看起来像是一个网址,但它是实际存在的,当go编译器在$GOPATH/src/github.com/mattn/go-sqlite3目录中能发现这个包时,你可以很简单的导入go-sqlite3包。

4.1.4 依赖管理

go get有一些其他的花样。如果我们在一个项目中执行go get,它会扫描所有文件并查找所有导入的第三方库,然后下载这些第三方库。某种程度上说,我们自己的源代码变成一个Gemfile或者package.json

执行go get -u将更新你的包(或者你可以通过go get -u FULL_PACKAGE_NAME更新指定的包)。

最后,你可能发现了go get的一些不足。首先,它不能指定一个修订,它会一直指向master/head/trunk/default。这是一个严重的问题,尤其当你有2个项目需要同一个库的不同版本时。

为了解决这个问题,你可以使用一个第三方的依赖管理工具。虽然还不太成熟,但是有2个依赖管理工具比较有前景,即goopgodep。更完整的列表可以参考go-wiki