1 命名空间和作用域
1.1 命名空间
命名空间(Namespace)在编程语言中常用来表示标识符(identifier)的可见范围。编程语言借助命名空间来解决标识符不能同名的问题,命名空间实际上相当于给标识符添加了标识前缀,使标识符变得全局唯一。另外,命名空间是程序组织更加模块化,降低了程序内部的耦合性。
一个标识符可以在多个命名空间中定义,它在不同命名空间中的含义是不互相干的。新的命名空间中可定义任意的标识符,它们不会与位于其他命名空间上的同名标识符发生冲突,当然自定义标识符尽量不要使用编程语言自身的关键字,因为这些标识符具有全局作用域。
Go语言继承了命名空间的概念,采用包来组织代码,包名构成Go命名空间的一部分,不同的包就是一个独立的命名空间。
Go语言除了包级显式的命名空间,还有隐式的命名空间。函数、方法,以及 if、for、switch 等和 “{ }” 一起构成了一个个代码块,代码块可以嵌套代码块,每一个代码块都构成一个隐式的命名空间。
不同命名空间可以声明相同的标识符,所以不同的隐式的命名空间同样允许声明相同的标识符(包括变量),这里就有变量覆盖的问题。在介绍变量覆盖之前,先来介绍作用域。
1.2 作用域
在高级编程语言中,作用域(scope)是指名字(name)与实体(可以理解为特定内存地址)的绑定(binding)保持有效的那部分程序逻辑区间。Go语言是静态作用域的编程语言。所谓静态作用域就是变量的作用域不依赖程序执行时的因素,变量作用域在编译器就能确定。
Go语言有三种类型的作用域:
- 全局作用域
在任何地方都可以访问的标识符,称其具有全局作用域。在Go语言中,全局作用域有两类:
(1)Go语言内置的预声明标识符(包括预声明的类型名、关键字、内置函数等),它们具有全局作用域,在任意命名空间内都可见。
(2)Go语言包内以大写字母开头的标识符(包括变量、常量、函数和方法名、自定义类型、结构体字段等),它们具有全局作用域,在任意命名空间内都可见。
- 包内作用域
在Go语言包内以小写字母开头的标识符(包括变量、常量、函数和方法名、自定义类型、结构体字段等),它们在本包可见,在其他包都是不可见的,这些标识符具有包内作用域。
- 隐式作用域
每个代码块内定义的变量称为“局部变量”,这些局部变量只在当前代码块内可见,其作用域属于当前代码块的隐式作用域。
1.3 变量覆盖
Go语言编译器解析变量名到引用实体采用的是从里到外的搜索模式,里层的局部变量能够覆盖掉外层变量,使得同名的外层变量不可见,这种现象称为变量覆盖。
由于命名空间允许同名变量的存在,大量的同名变量可能会给程序的可读性带来影响,Go语言通过包、代码缩进和大括号“{ }” 组织嵌套的方式来改善程序的可读性,这也是众多编程语言的通用做法。一些临时变量可以同名,一些关键变量还是尽量起一个有意义的名称。
2 包的基本概念
2.1 基本概念
Go语言的源码复用是建立在包(package)基础之上。Go语言的入库 main() 函数所在的包(package)叫 main 包,main包想要引用别的代码,必须同样以包的方式进行引用。Go语言使用包来组织代码的,并实现命名空间的管理。任何源代码文件必须属于某个包。源码文件的第一行有效代码必须是 package pkgName 语句,通过该语句声明自己所在的包。
Go语言的包借助了目录树的组织形式,一般包的名称就是其源文件所在目录的名称,虽然Go没有强制包名必须和其所在的目录名同名,但还是建议包名和所在目录同名,这样结构更清晰。包可以定义在很深的目录中,包的定义是不包括路径的,但是包的引用一般是全路径引用。比如在 $GOPATH/src/a/b 下定义一个包 c,在包 c 的源码中只需要声明为 package c,而不是声明为 package a/b/c,但是在 import 包 c 时,需要带上路径 import a/b/c。包的引用有两种形式,在下面的内容中会详细介绍。
包的习惯用法:
- 包名一般是小写的,使用一个简短的命名。
- 包名一般要和所在目录名同名。
- 包一般放在公司的域名目录下,这样能保证包名的唯一性,便于共享代码。比如个人的GitHub项目的包一般放到 $GOPATH/src/github.com/userName/projectName 目录下。
2.2 工作目录(GOPATH)
Go语言的包名和文件夹(Linux系统称为目录)是一一对应的,所有与包相关的操作,必须依赖于工作目录($GOPATH)。
GOPATH 是Go语言中的一个环境变量,它使用绝对路径提供项目的工作目录。工作目录是一个工程开发的相对参考目录。GOPATH 适合处理大量Go语言源码、多个包组合而成的复杂项目。对于比较大的项目开发,如果不使用工作目录,在多人协同开发时,每个人有一套自己的目录结构,读取配置文件的位置不统一,输出的二进制运行文件也不统一,这样会导致开发的标准不统一,影响开发效率。
<提示> C、C++、Java、C# 及其他编程语言发展到后期,都拥有自己的 IDE(集成开发环境),并且工程(Project)、解决方案(Solution)和 工作区(Workspace)等概念将源码和资源组织了起来,,方便编译和输出。
2.2.1 使用命令行查看 GOPATH 信息
在命令行下运行 go env 命令查看Go开发包的环境配置信息,这些配置信息里可以查看到当前的 GOPATH 路径设置情况。
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/kobe/.cache/go-build"
GOENV="/home/kobe/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/kobe/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/kobe/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build058793071=/tmp/go-build -gno-record-gcc-switches"
命令行说明如下:
- 执行 go env 命令,将输出当前Go开发包的环境变量状态。
- GOARCH 表示目标处理器硬件架构,这里是 x64 架构。
- GOBIN 表示编译器和链接器的安装位置。
- GOOS 表示目标操作系统,这里是 Linux 操作系统。
- GOPATH 表示当前工作目录,默认是用户名主目录下的go目录,例如我的是:"/home/kobe/go"。
- GOROOT 表示Go开发包的安装目录,我这里的安装路径是:"/usr/local/go"
在Go 1.8 版本之前,GOPATH 环境变量默认是空的。从Go 1.8 版本开始,Go开发包在安装完成之后,将 GOPATH 赋予了一个默认的目录,参见下表所示:
平台 | GOPATH 默认值 | 举例 |
Windows 平台 | %USERPROFILE%\go | C:\User\用户名\go |
Linux/Unix 平台 | $HOME/go | /home/用户名/go |
2.2.2 使用 GOPATH 的工程结构
在 GOPATH 指定的工作目录下,代码总是会保存在 $GOPATH/src 目录下。在工程经过 go build、go install 或 go get 等命令后,会将产生的二进制可执行文件放在 $GOPATH/bin 目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg 目录下。
如果需要将整个源码添加到版本控制工具(Version Control System,VCS)中时,只需要添加 $GOPATH/src 目录的源码即可。bin 和 pkg 目录的内容都可以由 src 目录生成。
2.2.3 设置和使用 GOPATH
我们以Linux平台为例,演示使用 GOPATH 的方法。
1. 设置当前目录为 GOPATH
选择一个目录,在目录中的命令行中执行下面的命令:
$ export GOPATH=`pwd`
$ echo $GOPATH
/home/kobe/go_work
使用 export 命令可以将当前目录的值设置到环境变量 GOPATH 中。
2. 建立GOPATH 的源码目录 src
使用下面的命令创建 GOPATH 中的 src 目录,在 src 目录下还有一个 hello 目录,该目录用于保存项目源码。
$ pwd
/home/kobe/go_work
$ mkdir -p src/hello
$ cd src/;ls
hello
3. 添加 main.go 源码文件
将 main.go 源码文件保存到 $GOPATH/src/hello 目录下。main.go 源码内容如下:
package main
import (
"fmt"
)
func main() {
fmt.Println("hello golang")
}
4. 编译源码并运行
由于此时我们已经设置好了环境变量 GOPATH,因此在Go语言中可以通过 GOPATH 找到工程的位置。在命令行中执行如下命令编译源码:
$ pwd
/home/kobe/go_work
$ go install hello # 编译项目名
$ cd bin/;ls # 编译完成后的可执行文件会保存在$GOPATH/bin目录下
hello
$ ./hello # 在bin目录中执行 ./hello,命令行输出结果如下
hello golang
2.2.4 在多项目工程中使用 GOPATH
当我们有多个相互独立的项目同时进行开发时,比如说:项目A的源码保存在 /home/kobe/projectA 目录下,而项目B的源码保存在 /home/kobe/projectB 目录下。在这样的情况下,建议无论是使用命令行或者集成开发环境编译Go的项目源码时,GOPATH 跟随项目而定。在 JetBrain 公司的 GoLand 集成开发环境(IDE)中的 GOPATH 设置为全局 GOPATH 和 项目 GOPATH。
GoLand 中的 Global GOPATH 代表全局 GOPATH,一般来源于系统环境变量中的 GOPATH;Project GOPATH 代表项目所在的 GOPATH,该设置会被保存在工作目录的 .idea 目录下,不会被设置到环境变量 GOPATH 中,但会在编译时使用到这个目录。建议在开发时只填写项目 GOPATH,每一个项目尽量只设置一个 GOPATH,不要使用多个 GOPATH 和全局的 GOPATH。
2.3 包引用
标准包的源码位于 $GOROOT/src 目录下,标准包可以直接引用。自定义的包和第三方包的源码必须放到 $GOPATH/src 目录下才能被引用。
包引用路径
包的引用路径有两种写法,一种是全路径,另一种是相对路径。
- 全路径引用
包的绝对路径就是 “$GOROOT/src 或 $GOPATH/src” 路径后包的源码路径的全路径,比如下面的包引用:
import "lab/test"
import "database/sql"
import "database/sql/driver"
test 包是自定义的包,其源码位于 $GOPATH/src/lab/test 目录下;sql 和 driver 包的源码分别位于 $GOROOT/src/database/sql 和 $GOROOT/src/database/sql/driver 下。
- 相对路径引用
相对路径引用只用于引用 $GOPATH/src 下的包,标准包的引用只能使用全路径引用。比如下面两个包:包 a 的源码路径是 $GOPATH/src/lab/a,包 b 的源码路径为 $GOPATH/src/lab/b,假设包 b 引用了包 a,则可以使用相对路径引用方式。示例如下:
// 在包b中使用相对路径方式引用包a
import "../a"
// 在包b中使用全路径方式引用包a
import "lab/a"
包引用格式
包引用有四种格式,为叙述方便,我们以标准库中的 fmt 包为例进行说明。
- 标准引用方式
import "fmt"
此时可以用 “fmt.” 作为前缀引用包内的可导出元素,这是常见的一种方式。例如:fmt.Println()。
- 别名引用方式
import F "fmt"
此时相对于给包 fmt 起了一个别名 F,用 "F." 代替标准的 "fmt." 作为前缀引用 fmt 包内可导出的元素。
- 省略方式
import . "fmt"
此时相对于把包 fmt 的命名空间直接合并到当前程序的命名空间中,使用 fmt 包内的可导出元素可以不用前缀 "fmt." ,直接引用。示例如下:
package main
import . "fmt"
func main() {
//不需要加前缀 fmt.
Println("hello, world")
}
- 仅执行包初始化 init() 函数
使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init() 初始化函数,则通过 import _ "pkgName" 这种方式引用包,仅执行包的初始化函数,即使没有 init() 函数,也不会引发编译器报错。示例如下:
import _ "fmt"
<注意事项>
- 一个包可以有多个 init() 函数,包加载会执行全部的 init 函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init 函数,将需要初始化的逻辑放到一个 init 函数里面。
- 包不能出现环形引用。比如:包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
- 包的重复引用是允许的,比如:包 a 引用了包 b 和包 c,包 b 和 包 c 都引用了包 d,这种场景下相当于包 a 重复引用了包 d,这种情况是允许的,并且Go编译器保证包 d 的 init 函数只会执行一次。
2.4 包加载
Go程序的启动/加载过程,在执行 main.main 之前,Go引导程序会先对整个程序的包进行初始化。整个执行的流程如下图所示:
![Go包的初始化](https://i-blog.csdnimg.cn/blog_migrate/aa1f07a9327257c0af5f0d5d4b88402a.png)
Go包的初始化有如下特点:
(1)包初始化程序从 main 函数引用的包开始,逐级检查包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图。
(2)Go编译器会将有向无环图转换为一颗树,然后从树的叶子结点开始逐层向上对包进行初始化。
(3)单个包的初始化过程如上图所示,先初始化常量,然后是全局变量,最后执行包的 init 函数(如果有的话)。
示例:包导入后的 init 函数初始化顺序。创建一个Go项目 pkginit。
$ echo $GOPATH //查看一下当前的GOPATH环境变量的值
/home/kobe/go_work
$ pwd
/home/kobe/go_work/src
$ mkdir pkginit //在$GOPATH/src目录下创建项目目录名pkginit
$ cd pkginit //进入项目目录下
$ mkdir pkg1 pkg2 //创建两个包名目录
$ ls
pkg1 pkg2
在 pkginit/pkg1 目录下添加源文件:pkg1.go,代码如下:
package pkg1
import (
"fmt"
"pkginit/pkg2" //在包pkg1中引用了包pkg2
)
func ExecPkg1() {
fmt.Println("ExecPkg1")
pkg2.ExecPkg2()
}
func init() {
fmt.Println("pkg1 init")
}
在 pkginit/pkg2 目录下添加源文件:pkg2.go,代码如下:
package pkg2
import "fmt"
func ExecPkg2() {
fmt.Println("ExecPkg2")
}
func init() {
fmt.Println("pkg2 init")
}
在 pkginit 目录下添加源文件:main.go,代码如下:
package main
import "pkginit/pkg1" //main包中引用了pkg1包
func main() {
pkg1.ExecPkg1()
}
查看一下 pkginit 项目的目录树结构:
$ tree -L 2 pkginit
pkginit
├── main.go
├── pkg1
│ └── pkg1.go
└── pkg2
└── pkg2.go
2 directories, 3 files
执行编译命令并运行,在命令行执行命令如下:
$ go install pkginit
$ cd ~/go_work/bin/;ls #可以看到,在$GOPATH/bin目录下生成了可执行文件pkginit
pkginit
$ ./pkginit #执行可执行文件
pkg2 init
pkg1 init
ExecPkg1
ExecPkg2
《代码说明》从运行结果可以看出,包的引用关系为: main-->pkg1-->pkg2,那么这些包的 init() 函数的调用顺序为: pkg2.init()-->pkg1.init()-->main。
3 第三方包管理 —— go module
Go语言的包由于采用目录布局的方式,在管理工程自身源码方面结合版本控制工具很方便,没有任何问题。现代软件工程一个很重要的理念就是代码复用,在软件项目开发中不可能完全基于标准库构建,会使用大量第三方的包和工具。Go语言开发的工程中引用大量第三方库也是一个很普遍的现象。
使用 go get 命令可以轻易地下载并安装第三方的库到本地,但这只是第一步。如果第三方库更新,并且新旧版本不兼容怎么办?因此,Go开发人员迫切需要对第三方库进行精细管理,也就是对项目工程使用的第三方库的版本做精确的管控。
谷歌公司采用的是集中式的版本管理,身在谷歌的Go核心开发人员并不能体会到其他Go开发人员对包的版本管理功能的迫切需求。在很长一段时间内,Go官方并没有第三方包管理的解决方案,直到 Go 1.5 版本引入了 vendor,为Go外部包的管理提供了有限的支持。在Go 1.11 和 Go 1.12 版本中引入了 Go Modules 机制,提供了统一的依赖包管理工具 go mod,依赖包统一收集在 $GOPATH/pkg/mod 中进行集中管理,并且将 import 路径与项目代码的实际存放路径解耦,使 package 定义导入更加灵活。
而从 Go 1.13 版本(released 2019/09/03)开始,Go Modules 机制成为了默认的依赖包管理方法。因此,这里我们只介绍目前最新的 go modules 包管理机制。
待续 ......
参考
《Go语言核心编程》
《Go语言从入门到进阶实战(视频教学版)》