# 9.5 自定义包和可见性

包是 Go 语言中代码组织和代码编译的主要方式。关于它们的很多基本信息已经在 4.2 章节中给出,最引人注目的便是可见性。现在我们来看看具体如何来使用自己写的包。在下一节,我们将回顾一些标准库中的包,自定义的包和标准库以外的包。

当写自己包的时候,要使用短小的不含有 _(下划线)的小写单词来为文件命名。这里有个简单例子来说明包是如何相互调用以及可见性是如何实现的。

当前目录下(examples/chapter_9/book/)有一个名为 package_mytest.go 的程序, 它使用了自定义包 pack1 中 pack1.go 的代码。这段程序(连同编译链接生成的 pack1.a)存放在当前目录下一个名为 pack1 的文件夹下。所以链接器将包的对象和主程序对象链接在一起。

示例 9.4 pack1.go

package pack1
var Pack1Int int = 42
var pack1Float = 3.14

func ReturnStr() string {
	return "Hello main!"
}
1
2
3
4
5
6
7

它包含了一个整型变量 Pack1Int 和一个返回字符串的函数 ReturnStr。这段程序在运行时不做任何的事情,因为它没有一个 main 函数。

在主程序 package_mytest.go 中这个包通过声明的方式被导入, 只到包的目录一层。

import "./pack1"
1

import 的一般格式如下:

import "包的路径或 URL 地址" 

例如:

import "github.com/org1/pack1”

路径是指当前目录的相对路径。

示例 9.5 package_mytest.go

package main

import (
	"fmt"
	"./pack1"
)

func main() {
	var test1 string
	test1 = pack1.ReturnStr()
	fmt.Printf("ReturnStr from package1: %s\n", test1)
	fmt.Printf("Integer from package1: %d\n", pack1.Pack1Int)
	// fmt.Printf("Float from package1: %f\n", pack1.pack1Float)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

输出结果:

ReturnStr from package1: Hello main!
Integer from package1: 42

如果包 pack1 和我们的程序在同一路径下,我们可以通过 "import ./pack1" 这样的方式来引入,但这不被视为一个好的方法。

下面的代码试图访问一个未引用的变量或者函数,甚至没有编译。将会返回一个错误:

fmt.Printf("Float from package1: %f\n", pack1.pack1Float)
1

错误:

cannot refer to unexported name pack1.pack1Float

主程序利用的包必须在主程序编写之前被编译。主程序中每个 pack1 项目都要通过包名来使用:pack1.Item。具体使用方法请参见示例 4.6 和 4.7。

因此,按照惯例,子目录和包之间有着密切的联系:为了区分,不同包存放在不同的目录下,每个包(所有属于这个包中的 go 文件)都存放在和包名相同的子目录下:

Import with . :

import . "./pack1"

当使用.来做为包的别名时,你可以不通过包名来使用其中的项目。例如:test := ReturnStr()

在当前的命名空间导入 pack1 包,一般是为了具有更好的测试效果。

Import with _ :

import _ "./pack1/pack1"

pack1包只导入其副作用,也就是说,只执行它的init函数并初始化其中的全局变量。

导入外部安装包:

如果你要在你的应用中使用一个或多个外部包,首先你必须使用 go install(参见第 9.7 节)在你的本地机器上安装它们。

假设你想使用 http://codesite.ext/author/goExample/goex 这种托管在 Google Code、GitHub 和 Launchpad 等代码网站上的包。

你可以通过如下命令安装:

go install codesite.ext/author/goExample/goex

将一个名为 codesite.ext/author/goExample/goex 的 map 安装在 $GOROOT/src/ 目录下。

通过以下方式,一次性安装,并导入到你的代码中:

import goex "codesite.ext/author/goExample/goex"

因此该包的 URL 将用作导入路径。

http://golang.org/cmd/goinstall/go install 文档中列出了一些广泛被使用的托管在网络代码仓库的包的导入路径

包的初始化:

程序的执行开始于导入包,初始化 main 包然后调用 main 函数。

一个没有导入的包将通过分配初始值给所有的包级变量和调用源码中定义的包级 init 函数来初始化。一个包可能有多个 init 函数甚至在一个源码文件中。它们的执行是无序的。这是最好的例子来测定包的值是否只依赖于相同包下的其他值或者函数。

init 函数是不能被调用的。

导入的包在包自身初始化前被初始化,而一个包在程序执行中只能初始化一次。

编译并安装一个包(参见第 9.7 节):

在 Linux/OS X 下可以用类似第 3.9 节的 Makefile 脚本做到这一点:

include $(GOROOT)/src/Make.inc
TARG=pack1
GOFILES=\
 	pack1.go\
 	pack1b.go\
include $(GOROOT)/src/Make.pkg

通过 chmod 777 ./Makefile确保它的可执行性。

上面脚本内的include语引入了相应的功能,将自动检测机器的架构并调用正确的编译器和链接器。

然后终端执行 make 或 gomake 工具:他们都会生成一个包含静态库 pack1.a 的 _obj 目录。

go install(参见第 9.7 节,从 Go1 的首选方式)同样复制 pack1.a 到本地的 $GOROOT/pkg 的目录中一个以操作系统为名的子目录下。像 import "pack1" 代替 import "path to pack1",这样只通过名字就可以将包在程序中导入。

当第 13 章我们遇到使用测试工具进行测试的时候我们将重新回到自己的包的制作和编译这个话题。

问题 9.1

a)一个包能分成多个源文件么?

b)一个源文件是否能包含多个包?

练习 9.3

创建一个程序 main_greetings.go 能够和用户说 "Good Day" 或者 "Good Night"。不同的问候应该放到单独的 greetings 包中。

在同一个包中创建一个 IsAM 函数返回一个布尔值用来判断当前时间是 AM 还是 PM,同样创建 IsAfternoonIsEvening 函数。

使用 main_greetings 作出合适的问候(提示:使用 time 包)。

练习 9.4 创建一个程序 main_oddven.go 判断前 100 个整数是不是偶数,将判断所用的函数编写在 even 包里。

练习 9.5 使用第 6.6 节的斐波那契程序:

1)将斐波那契功能放入自己的 fibo 包中并通过主程序调用它,存储最后输入的值在函数的全局变量。

2)扩展 fibo 包将通过调用斐波那契的时候,操作也作为一个参数。实验 "+" 和 “*”

main_fibo.go / fibonacci.go