Go 语言的源码复用创建在包(package)基础之上。Go 语言的入口 main() 函数所在的包(package)叫 main,main 包想要引用别的代码,必须一样以包的方式进行引用,本章内容将详细讲解如何导出包的内容及如何导入其余包。
Go 语言的包与文件夹一一对应,全部与包相关的操做,必须依赖于工做目录(GOPATH)。java
GOPATH 是 Go 语言中使用的一个环境变量,它使用绝对路径提供项目的工做目录。
工做目录是一个工程开发的相对参考目录,比如当你要在公司编写一套服务器代码,你的工位所包含的桌面、计算机及椅子就是你的工做区。工做区的概念与工做目录的概念也是相似的。若是不使用工做目录的概念,在多人开发时,每一个人有一套本身的目录结构,读取配置文件的位置不统一,输出的二进制运行文件也不统一,这样会致使开发的标准不统一,影响开发效率。
GOPATH 适合处理大量 Go 语言源码、多个包组合而成的复杂工程。linux
C、C++、Java、C# 及其余语言发展到后期,都拥有本身的 IDE(集成开发环境),而且工程(Project)、解决方案(Solution)和工做区(Workspace)等概念将源码和资源组织了起来,方便编译和输出。golang
在安装过 Go 开发包的操做系统中,可使用命令行查看 Go 开发包的环境变量配置信息,这些配置信息里能够查看到当前的 GOPATH 路径设置状况。在命令行中运行go env
后,命令行将提示如下信息:缓存
$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/davy/go"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0"
CXX="g++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"服务器
命令行说明以下:架构
从命令行输出中,能够看到 GOPATH 设定的路径为:/home/davy/go(davy 为笔者的用户名)。
在 Go 1.8 版本以前,GOPATH 环境变量默认是空的。从 Go 1.8 版本开始,Go 开发包在安装完成后,将 GOPATH 赋予了一个默认的目录,参见下表。
编辑器
平 台 | GOPATH 默认值 | 举 例 |
---|---|---|
Windows 平台 | %USERPROFILE%/go | C:\Users\用户名\go |
Unix 平台 | $HOME/go | /home/用户名/go |
在 GOPATH 指定的工做目录下,代码老是会保存在 $GOPATH/src 目录下。在工程通过 go build、go install 或 go get 等指令后,会将产生的二进制可执行文件放在 $GOPATH/bin 目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg 下。
若是须要将整个源码添加到版本管理工具(Version Control System,VCS)中时,只须要添加 $GOPATH/src 目录的源码便可。bin 和 pkg 目录的内容均可以由 src 目录生成。ide
本节以 Linux 为演示平台,为你们演示使用 GOPATH 的方法。函数
选择一个目录,在目录中的命令行中执行下面的指令:工具
export GOPATH=`pwd`
该指令中的 pwd 将输出当前的目录,使用反引号`
将 pwd 指令括起来表示命令行替换,也就是说,使用`pwd`
将得到 pwd 返回的当前目录的值。例如,假设你的当前目录是“/home/davy/go”,那么使用`pwd`
将得到返回值“/home/davy/go”。
使用 export 指令能够将当前目录的值设置到环境变量 GOPATH中。
使用下面的指令建立 GOPATH 中的 src 目录,在 src 目录下还有一个 hello 目录,该目录用于保存源码。
mkdir -p src/hello
mkdir 指令的 -p 能够连续建立一个路径。
使用 Linux 编辑器将下面的源码保存为 main.go 并保存到 $GOPATH/src/hello 目录下。
此时咱们已经设定了 GOPATH,所以在 Go 语言中能够经过 GOPATH 找到工程的位置。
在命令行中执行以下指令编译源码:
go install hello
编译完成的可执行文件会保存在 $GOPATH/bin 目录下。
在 bin 目录中执行 ./hello,命令行输出以下:
hello world
在不少与 Go 语言相关的书籍、文章中描述的 GOPATH 都是经过修改系统全局的环境变量来实现的。然而,根据笔者多年的 Go 语言使用和实践经验及周边朋友、同事的反馈,这种设置全局 GOPATH 的方法可能会致使当前项目错误引用了其余目录的 Go 源码文件从而形成编译输出错误的版本或编译报出一些没法理解的错误提示。
好比说,将某项目代码保存在 /home/davy/projectA 目录下,将该目录设置为 GOPATH。随着开发进行,须要再次获取一份工程项目的源码,此时源码保存在 /home/davy/projectB 目录下,若是此时须要编译 projectB 目录的项目,但开发者忘记设置 GOPATH 而直接使用命令行编译,则当前的 GOPATH 指向的是 /home/davy/projectA 目录,而不是开发者编译时指望的 projectB 目录。编译完成后,开发者就会将错误的工程版本发布到外网。
所以,建议你们不管是使用命令行或者使用集成开发环境编译 Go 源码时,GOPATH 跟随项目设定。在 Jetbrains 公司的 GoLand 集成开发环境(IDE)中的 GOPATH 设置分为全局 GOPATH 和项目 GOPATH,以下图所示。
图中的 Global GOPATH 表明全局 GOPATH,通常来源于系统环境变量中的 GOPATH;Project GOPATH 表明项目所使用的 GOPATH,该设置会被保存在工做目录的 .idea 目录下,不会被设置到环境变量的 GOPATH 中,但会在编译时使用到这个目录。建议在开发时只填写项目 GOPATH,每个项目尽可能只设置一个 GOPATH,不使用多个 GOPATH 和全局的 GOPATH。
Visual Studio 早期在设计时,容许 C++ 语言在全局拥有一个包含路径。当一个工程多个版本的编译,或者两个项目混杂有不一样的共享全局包含时,会发生难以察觉的错误。在新版本 Visual Studio 中已经废除了这种全局包含的路径设计,并建议开发者将包含目录与项目关联。
Go 语言中的 GOPATH 也是一种相似全局包含的设计,所以鉴于 Visual Studio 在设计上的失误,建议开发者不要设置全局的 GOPATH,而是随项目设置 GOPATH。
包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go 语言默认为咱们提供了不少包,如 fmt、os、io 包等,开发者能够根据本身的须要建立本身的包。
包要求在同一个目录下的全部文件的第一行添加以下代码,以标记该文件归属的包:
package 包名
包的特性以下:
在 Go 语言中,若是想在一个包里引用另一个包里的标识符(如类型、变量、常量等)时,必须首先将被引用的标识符导出,将要导出的标识符的首字母大写就可让引用者能够访问这些标识符了。
下面代码中包含一系列未导出标识符,它们的首字母都为小写,这些标识符能够在包内自由使用,可是包外没法访问它们,代码以下:
将 myStruct 和 myConst 首字母大写,导出这些标识符,修改后代码以下:
此时,MyConst 和 MyStruct 能够被外部访问,而 myVar 因为首字母是小写,所以只能在 mypkg 包内使用,不能被外部包引用。
在被导出的结构体或接口中,若是它们的字段或方法首字母是大写,外部能够访问这些字段和方法,代码以下:
在代码中,MyStruct 的 ExportedField 和 MyInterface 的 ExportedMethod() 能够被包外访问。
要引用其余包的标识符,可使用 import 关键字,导入的包名使用双引号包围,包名是从 GOPATH 开始计算的路径,使用/
进行路径分隔。
导入有两种基本格式,即单行导入和多行导入,两种导入方法的导入代码效果是一致的。
单行导入格式以下:
import "包1"
import "包2"
当多行导入时,包名在 import 中的顺序不影响导入效果,格式以下:
import(
"包1"
"包2"
…
)
参考代码 8-1 的例子来理解 import 的机制。
代码 8-1 的目录层次以下:
.
└── src
└── chapter08
└── importadd
├── main.go
└── mylib
└── add.go
代码8-1 加函数(具体文件:…/chapter08/importadd/mylib/add.go)
第 3 行中的 Add() 函数以大写 A 开头,表示将 Add() 函数导出供包外使用。当首字母小写时,为包内使用,包外没法引用到。
add.go 在 mylib 文件夹下,习惯上将文件夹的命名与包名一致,命名为 mylib 包。
代码8-2 导入包(具体文件:…/chapter08/importadd/main.go)
代码说明以下:
在命令行中运行下面代码:
export GOPATH=/home/davy/golangbook/code
go install chapter08/importadd
$GOPATH/bin/importadd
命令说明以下:
运行代码,输出结果以下:
3
在默认导入包的基础上,在导入包路径前添加标识符便可造成自定义引用包,格式以下:
customName "path/to/package"
其中,path/to/package 为要导入的包路径,customName 为自定义的包名。
在 code8-1 的基础上,在 mylib 导入的包名前添加一个标识符,代码以下:
代码说明以下:
若是只但愿导入包,而不使用任何包内的结构和类型,也不调用包内的任何函数时,可使用匿名导入包,格式以下:
其中,path/to/package 表示要导入的包名,下画线_
表示匿名导入包。
匿名导入的包与其余方式导入包同样会让导入包编译到可执行文件中,同时,导入包也会触发 init() 函数调用。
在某些需求的设计上须要在程序启动时统一调用程序引用到的全部包的初始化函数,若是须要经过开发者手动调用这些初始化函数,那么这个过程可能会发生错误或者遗漏。咱们但愿在被引用的包内部,由包的编写者得到代码启动的通知,在程序启动时作一些本身包内代码的初始化工做。
例如,为了提升数学库计算三角函数的执行效率,能够在程序启动时,将三角函数的值提早在内存中建成索引表,外部程序经过查表的方式迅速得到三角函数的值。可是三角函数索引表的初始化函数的调用不但愿由每个外部使用三角函数的开发者调用,若是在三角函数的包内有一个机制能够告诉三角函数包程序什么时候启动,那么就能够解决初始化的问题。
Go 语言为以上问题提供了一个很是方便的特性:init() 函数。
init() 函数的特性以下:
例如,假设有这样的包引用关系:main→A→B→C,那么这些包的 init() 函数调用顺序为:
C.init→B.init→A.init→main
说明:
Go 语言包会从 main 包开始检查其引用的全部包,每一个包也可能包含其余的包。Go 编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
在运行时,被最后导入的包会最早初始化并调用 init() 函数。
经过下面的代码理解包的初始化顺序。
代码8-3 包导入初始化顺序入口(…/chapter08/pkginit/main.go)
代码说明以下:
代码8-4 包导入初始化顺序pkg1(…/chapter08/pkginit/pkg1/pkg1.go)
代码说明以下:
代码8-5 包导入初始化顺序pkg2(…/chapter08/pkginit/pkg2/pkg2.go)
代码说明以下:
执行代码,输出以下:
pkg2 init
pkg1 init
ExecPkg1
ExecPkg2
本例利用包的 init 特性,将 cls1 和 cls2 两个包注册到工厂,使用字符串建立这两个注册好的结构实例。
完整代码的结构以下:
.
└── src
└── chapter08
└── clsfactory
├── main.go
└── base
└── factory.go
└── cls1
└── reg.go
└── cls2
└── reg.go
类工厂(具体文件:…/chapter08/clsfactory/base/factory.go)
这个包叫base,负责处理注册和使用工厂的基础代码,该包不会引用任何外部的包。
如下是对代码的说明:
func() Class
的普通函数,调用此函数,建立一个类实例,实现的工厂内部结构体会实现 Class 接口。
类1及注册代码(具体文件:…/chapter08/clsfactory/cls1/reg.go)
上面的代码展现了Class1的工厂及产品定义过程。
类2及注册代码(具体文件:…/chapter08/clsfactory/cls2/reg.go)
Class2 的注册与 Class1 的定义和注册过程相似。
类工程主流程(具体文件:…/chapter08/clsfactory/main.go)
下面是对代码的说明:
执行下面的指令进行编译:
export GOPATH=/home/davy/golangbook/code
go install chapter08/clsfactory
$GOPATH/bin/clsfactory
代码输出以下: Class1 Class2