网站地图    收藏    合作   
<

快捷菜单 返回顶部

Go语言是一门推崇软件工程理念的编程语言,它为开发周期的每个环节都提供了完备的工具和支持。Go语言高度强调代码和项目的规范和统一,这集中体现在工程结构或者说代码体制的细节之处。Go 也是一门开放的语言,它本身就是开源软件。

更重要的是,Go语言让开发人员很容易通过 go get 命令从各种公共代码库(比如 GitHub)中下载开源代码并使用。这除了得益于 Go语言自带命令的强大之外,还应该归功于 Go 工程结构的严谨和完善。本节中,我们详述 Go语言的工程结构。

工作区

一般情况下,Go语言的源码文件必须放在工作区中。但是对于命令源码文件来说,这不是必需的。工作区其实就是一个对应于特定工程的目录,它应包含 3 个子目录:src 目录、pkg 目录和 bin 目录,下面逐一说明。

src 目录

用于以代码包的形式组织并保存 Go 源码文件,这里的代码包与 src 下的子目录一一对应。例如,若一个源码文件被声明属于代码包 log,那么它就应当保存在 src/log 目录中。

当然,也可以把 Go 源码文件直接放在 src 目录下,但这样的 Go 源码文件就只能被声明属于 main 代码包了。除非用于临时测试或演示,一般还是建议把 Go 源码文件放入特定的代码包中。

pkg 目录

用于存放通过 go install 命令安装后的代码包的归档文件。前提是代码包中必须包含 Go 库源码文件。归档文件是指那些名称以“.a”结尾的文件。该目录与 GOROOT 目录下的 pkg 目录功能类似。区别在于,工作区中的 pkg 目录专门用来存放用户代码的归档文件。

编译和安装用户代码的过程一般会以代码包为单位进行。比如 log 包被编译安装后,将生成一个名为 log.a 的归档文件,并存放在当前工作区的 pkg 目录下的平台相关目录中。

bin 目录

与 pkg 目录类似,在通过 go install 命令完成安装后,保存由 Go 命令源码文件生成的可执行文件。在类 Unix 操作系统下,这个可执行文件一般来说名称与源码文件的主文件名相同。而在 Windows 操作系统下,这个可执行文件的名称则是源码文件主文件名加 .exe 后缀。

注意:这里有必要明确一下 Go语言的命令源码文件和库源码文件的区别。所谓命令源码文件,就是声明属于 main  代码包并且包含无参数声明和结果声明的 main 函数的源码文件。这类源码文件是程序的入口,它们可以独立运行(使用 go run 命令),也可以通过 go build 或 go install 命令得到相应的可执行文件。所谓库源码文件,则是指存在于某个代码包中的普通源码文件。

GOPATH

我们需要将工作区的目录路径添加到环境变量 GOPATH 中。否则,即使处于同一工作区,代码之间也无法通过绝对代码包路径调用。在实际开发环境中,工作区可以只有一个,也可以有多个,这些工作区的目录路径都需要添加到 GOPATH 中。与 GOROOT 一样,我们应该确保 GOPATH 一直有效。

需要注意一下两点:

源码文件

Go 的源码文件有 3 个种类,即命令源码文件、库源码文件和测试源码文件,下面详细说明这 3 类源码文件。

1) 命令源码文件

如果一个源码文件被声明属于 main 代码包,且该文件代码中包含无参数声明和结果声明的 main 函数,则它就是命令源码文件。命令源码文件可通过 go run 命令直接启动运行。

同一个代码包中的所有源码文件,其所属代码包的名称必须一致。如果命令源码文件和库源码文件处于同一个代码包中,那么在该包中就无法正确执行 go build 和 go install 命令。换句话说,这些源码文件将无法通过常规方法编译和安装。

因此,命令源码文件通常会单独放在一个代码包中。这是合理且必要的,因为通常一个程序模块或软件的启动入口只有一个。

同一个代码包中可以有多个命令源码文件,可通过 go run 命令分别运行,但这会使 go build 和 go install 命令无法编译和安装该代码包。所以,我们也不应该把多个命令源码文件放在同一个代码包中。

当代码包中有且仅有一个命令源码文件时,在文件所在目录中执行 go build 命令,即可在该目录下生成一个与目录同名的可执行文件;而若使用 go install 则可在当前工作区的 bin 目录下生成相应的可执行文件。例如,代码包 gopcp.v2/helper/ds 中只有一个源码文件 showds.go,且它是命令源码文件,则相关操作和结果如下:

hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/ds$ Is
showds.go
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/ds$ go build
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/ds$ Is
ds showds.go
hc@ubt: ~/golang/example.v2/src/gopcp.v2/helper/ds$ go install
hc@ubt:~/golang/example.v2/src/gopcp・v2/helper/ds$ Is ../../../../bin
ds

需要特别注意,只有当环境变量 GOPATH 中只包含一个工作区的目录路径时,go install 命令才会把命令源码文件安装到当前工作区的 bin 目录下;否则,像这样执行 go install 命令就会失败。此时必须设置环境变量 GOBIN,该环境变量的值是一个目录的路径,该目录用于存放所有因安装 Go 命令源码文件而生成的可执行文件。

2) 库源码文件

通常,库源码文件声明的包名会与它直接所属的代码包(目录)名一致,且库源码文件中不包含无参数声明和无结果声明的 main 函数。下面来安装(其中包含编译)gopcp.v2/helper/log 包,其中含有若干库源码文件:

hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ ls
base    logger.go    logger_test.go logrus
hc@ubt: ~/golang/example.v2/src/gopcp.v2/helper/log$ go install
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ ls    ../../../pkg
linux_amd64
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ ls    ../../../pkg/linux_amd64/gopcp.v2/helper
log    log.a
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ ls ../../../../pkg/linux_amd64/gopcp.v2/helper/log
base.a    field.a    logrus.a

这里,我们通过在 gopcp.v2/helper/log 代码包的目录下执行 go install 命令,成功安装了该包并生成了若干归档文件。这些归档文件的存放目录由以下规则产生。

安装库源码文件时所生成的归档文件会被存放到当前工作区的 pkg 目录中。example.v2 项目的 gopcp.v2/helper/log 包所属工作区的根目录是 ~/golang/example.v2。因此,上面所说的 pkg 目录即 ~/golang/example.v2/pkg。

根据被编译时的目标计算环境,归档文件会被放在该 pkg 目录下的平台相关目录中。例如,我是在 64 位的 Linux 计算环境下安装的,对应的平台相关目录就是 linux_amd64,那么归档文件一定会被存放到 ~/golang/example.v2/pkg/linux_amd64 目录中的某个地方。

存放归档文件的目录的相对路径与被安装的代码包的上一级代码包的相对路径一致。第一个相对路径是相对于工作区的 pkg 目录下的平台相关目录而言的,而第二个相对路径是相对于工作区的 src 目录而言的。如果被安装的代码包没有上一级代码包(也就是说,它的父目录就是工作区的 src 目录),那么它的归档文件就会被直接存放到当前工作区的 pkg 目录的平台相关目录下。

例如,gopcp.v2/helper/log 包的归档文件 log.a 一定会被存放到 ~/golang/example.v2/pkg/linux_amd64/gopcp.v2/helper 这个目录下。而它的子代码包 gopcp.v2/helper/log/base 的归档文件 base.a,则一定会被存放到 ~/golang/example.v2/pkg/linux_amd64/gopcp.v2/helper/log 目录下。

3) 测试源码文件

测试源码文件是一种特殊的库文件,可以通过执行 go test 命令运行当前代码包下的所有测试源码文件。成为测试源码文件的充分条件有两个,如下。

当在一个代码包中执行 go test 命令时,该代码包中的所有测试源码文件会被找到并运行。我们依然以 gopcp.v2/helper/log 包为例:

hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ go test
PASS
ok    gopcp.v2/helper/log    0.008s

这里使用 go test 命令在 gopcp.v2/helper/log 包中找到并运行了测试源码文件 logger_test.go,且调用其中所有的测试函数。命令行的回显信息表示我们通过了测试,并且运行测试源码文件中的测试程序共花费了 0.080 S。

最后插一句,Go 代码的文本文件需要以 UTF-8 编码存储。如果源码文件中出现了非 UTF-8 编码的字符,那么在运行、编译或安装的时候,Go 命令会抛出 illegal UTF-8 sequence 错误。

代码包

在 Go 中,代码包是代码编译和安装的基本单元,也是非常直观的代码组织形式。

1) 包声明

细心的读者可能已经发现,在 example.v2 项目的代码包中,多数源码文件名称看似都与包名没什么关系。实际上,在 Go语言中,代码包中的源码文件可以任意命名。另外,这些任意名称的源码文件都必须以包声明语句作为文件中代码的第一行。比如,gopcp.v2/helper/log/base 包中的所有源码文件都要先声明自己属于某一个代码包:

package "base"

其中 package 是 Go 中用于包声明语句的关键字。Go 规定包声明中的包名是代码包路径的最后一个元素。比如,gopcp.v2/helper/log/base 包的源码文件包声明中的包名是 base。但是,不论命令源码文件存放在哪个代码包中,它都必须声明属于 main 包。

2) 包导入

代码包 gopcp.v2/helper/log 中的 logger.go 需要依赖 base 子包和 logrus 子包,因此需要在源码文件中使用代码包导入语句,如下所示:

import "gopcp.v2/helper/log/base"
import "gopcp.v2/helper/log/logrus"

这需要用到代码包导入路径,即代码包在工作区的 src 目录下的相对路径。

当导入多个代码包时,可以用圆括号括起它们,且每个代码包名独占一行。在使用被导入代码包中公开的程序实体时,需要使用包路径的最后一个元素加的方式指定代码所在的包。

因此,上述语句可以写成:

import (
    "gopcp.v2/helper/log/base"
    "gopcp.v2/helper/log/logrus"
)

同一个源码文件中导入的多个代码包的最后一个元素不能重复,否则一旦使用其中的程序实体,就会引起编译错误。但是,如果你只导入不使用,同样会引起编译错误。一个解决方法是为其中一个起个别名,比如:

import (
    "github.com/Sirupsen/logrus"
    mylogrus "gopcp.v2/helper/log/logrus"
)

如果我们想不加前缀而直接使用某个依赖包中的程序实体,就可以用来代替别名,如下所示:

import (
    . "gopcp.v2/helper/log/logrus"
)

看到那个“.”了吗?之后,在当前源码文件中,我们就可以这样做了:

var logger = NewLogger("gopcp") // NewLogger 是 gopcp.v2/helper/log/logrus 包中的函数

这里强调一下,Go 中的变量、常量、函数和类型声明可统称为程序实体,而它们的名称统称为标识符。标识符可以是 Unicode 字符集中任意能表示自然语言文字的字符、数字以及下划线 (_)。标识符不能以数字或下划线开头。

实际上,标识符的首字符的大小写控制着对应程序实体的访问权限。如果标识符的首字符是大写形式,那么它所对应的程序实体就可以被本代码包之外的代码访问到,也称为可导出的或公开的;否则,对应的程序实体就只能被本包内的代码访问,也称为不可导岀的或包级私有的。要想成为可导出的程序实体,还需要额外满足以下两个条件。

代码包导入还有另外一种情况:如果只想初始化某个代码包,而不需要在当前源码文件中使用那个代码包中的任何程序实体,就可以用“_”来代替别名:

import (
    _ "github.com/Simpsen/logrus"
)

这种情况下,我们只是触发了这个代码包中的初始化操作(如果有的话)。符号“_”就像一个垃圾桶,它在代码中使用很广泛,在后续的学习中还可以看到它的身影。

3) 包初始化

在 Go语言中,可以有专门的函数负责代码包初始化,称为代码包初始化函数。这个函数需要无任何参数声明和结果声明,且名称必须为 init,如下所示:

func init() {
    fmt.Println("Initialize...")
}

Go 会在程序真正执行前对整个程序的依赖进行分析,并初始化相关的代码包。也就是说,所有的代码包初始化函数都会在 main 函数(命令源码文件中的入口函数)执行前执行完毕,而且只会执行一次。另外,对于每一个代码包来说,其中的所有全局变量的初始化,都会在代码包的初始化函数执行前完成。这避免了在代码包初始化函数对某个变量进行赋值之后,又被该变量声明中赋予的值覆盖掉的问题。

下面的代码展示了全局赋值语句、代码包初始化函数以及主函数的执行顺序。其中,双斜杠及其右边的内容为代码注释,Go 编译器在编译的时候会将其忽略。
package main //命令源码文件必须在这里声明自己属于main包

import ( //导入标准库代码包fmt和runtime
    "fmt"
    "runtime"
)
func init() { //代码包初始化函数
    fmt.Printf("Map: %v\n", m)  // 格式化的打印
    //通过调用runtime包的代码获取当前机器的操作系统和计算架构.
    //而后通过fmt包的Sprintf方法进行格式化字符串生成并赋值给变量info
    info = fmt.Sprintf("OS: %s, Arch: %s", runtime.GOOS, runtime.GOARCH)
}
//非局部变量,map类型,且已初始化
var m = map[int]string{l: "A", 2: "B", 3: "C"}
//非局部变量,string类型,未被初始化
var info string
func main() {         //命令源码文件必须有的入口函数,也称主函数
    fmt.Println(info) // 打印变量 info
}
运行这个文件:

hc@ubt:~/golang/example.v2/src/gopcp.v2/chapter1/pkginit$ go run pkg_init.go
Map: map[l:A 2:B 3:C]
OS: linux, Arch: amd64

关于每行代码的用途,在源码文件中我已经作了基本的解释。这里只解释这个小程 序的输出。

第一行是对变量 m 的值格式化后的结果。可以看到,在函数 init 的第一条语句执行时,变量 m 已经被初始化并赋值了。这验证了:当前代码包中所有全局变量的初始化会在代码包初始化函数执行前完成。

第二行是对变量 info 的值格式化后的结果。变量 info 被定义时并没有显式赋值,因此它被赋予类型 string 的零值——""(空字符串)。之后,变量 info 在代码包初始化函数 init 中被赋值,并在入口函数 main 中输出。这验证了:所有的代码包初始化函数都会在 main 函数执行前执行完毕。

同一个代码包中可以存在多个代码包初始化函数,甚至代码包内的每一个源码文件都可以定义多个代码包初始化函数。Go 不会保证同一个代码包中多个代码包初始化函数的执行顺序。此外,被导入的代码包的初始化函数总是会先执行。在上例中,fmt 和 runtime 包中的 init 函数(如果有的话)会先执行,然后当前文件中的 init 函数才会执行。

自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习

京ICP备14009008号@版权所有www.zixuephp.com

网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com