go的程序结构
# 4. Go程序的结构
# 4.1 hello,world 示例程序
每种编程语言都将一个名为“hello, world”的示例作为这门语言学习的第一个例子,这个传统始于 20 世纪 70 年代那本大名鼎鼎的由布莱恩·科尼根(Brian W. Kernighan)与 C 语言之父丹尼斯·里奇(Dennis M. Ritchie)合著的《C 程序设计语言》。
Go 的命名规则:Go 源文件总是用全小写字母形式的短小单词命名,并且以.go 扩展名结尾。
如果要在源文件的名字中使用多个单词,我们通常直接是将多个单词连接起来作为源文件名,而不是使用其他分隔符,比如下划线。也就是说,我们通常使用 helloworld.go 作为文件名而不是 hello_world.go。
这是因为下划线这种分隔符,在 Go 源文件命名中有特殊作用,这个我们会在以后的讲解中详细说明。总的来说,我们尽量不要用两个以上的单词组合作为文件名,否则就很难分辨了。
main.go:
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
2
3
4
5
6
7
编译和运行:
$go build main.go
$./main
hello, world
2
3
# 4.2 hello,world 示例程序的结构
main.go:
// 这一行代码定义了 Go 中的一个包 package。
// 包是 Go 语言的基本组成单元,通常使用单个的小写单词命名,一个 Go 程序本质上就是一组包的集合。所有 Go 代码都有自己隶属的包,在这里我们的“hello,world”示例的所有代码都在一个名为 main 的包中。
// main 包在 Go 中是一个特殊的包,整个 Go 程序中仅允许存在一个名为 main 的包。
// main 包中的主要代码是一个名为 main 的函数
package main
import "fmt"
// main 函数会比较特殊:当你运行一个可执行的 Go 程序的时候,所有的代码都会从这个入口函数开始运行。
// 这段代码的第一行声明了一个名为 main 的、没有任何参数和返回值的函数。如果某天你需要给函数声明参数的话,那么就必须把它们放置在圆括号 () 中。
// 另外,那对花括号{}被用来标记函数体,Go 要求所有的函数体都要被花括号包裹起来。按照惯例,我们推荐把左花括号与函数声明置于同一行并以空格分隔。
// Go 语言内置了一套 Go 社区约定俗称的代码风格,并随安装包提供了一个名为 Gofmt 的工具,这个工具可以帮助你将代码自动格式化为约定的风格。Gofmt 是 Go 语言在解决规模化(scale)问题上的一个最佳实践,并成为了 Go 语言吸引其他语言开发者的一大卖点。很多其他主流语言也在效仿 Go 语言推出自己的 format 工具,比如:Java formatter、Clang formatter、Dartfmt 等。因此,作为 Go 开发人员,请在提交你的代码前使用 Gofmt 格式化你的 Go 源码。
func main() {
fmt.Println("hello, world")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
**注意点 1:**标准 Go 代码风格使用 Tab 而不是空格来实现缩进的,当然这个代码风格的格式化工作也可以交由 gofmt 完成。
**注意点 2:**我们调用了一个名为 Println 的函数,这个函数位于 Go 标准库的 fmt 包中。为了在我们的示例程序中使用 fmt 包定义的 Println 函数,我们其实做了两步操作:
第一步是在源文件的开始处通过 import 声明导入 fmt 包的包路径:import "fmt"
第二步则是在 main 函数体中,通过 fmt 这个限定标识符(Qualified Identifier)调用 Println 函数。
虽然两处都使用了“fmt”这个字面值,但在这两处“fmt”字面值所代表的含义却是不一样的:
- import “fmt” 一行中“fmt”代表的是包的导入路径(Import),它表示的是标准库下的 fmt 目录,整个 import 声明语句的含义是导入标准库 fmt 目录下的包;
- fmt.Println 函数调用一行中的“fmt”代表的则是包名。
通常导入路径的最后一个分段名与包名是相同的,这也很容易让人误解 import 声明语句中的“fmt”指的是包名,其实并不是这样的。
main 函数体中之所以可以调用 fmt 包的 Println 函数,还有最后一个原因,那就是 Println 函数名的首字母是大写的。在 Go 语言中,只有首字母为大写的标识符才是导出的(Exported),才能对包外的代码可见;如果首字母是小写的,那么就说明这个标识符仅限于在声明它的包内可见。
另外,在 Go 语言中,main 包是不可以像标准库 fmt 包那样被导入(Import)的,如果导入 main 包,在代码编译阶段你会收到一个 Go 编译器错误:import “xx/main” is a program, not an importable package。
**注意点 3:**我们还是回到 main 函数体实现上,把关注点放在传入到 Println 函数的字符串“hello, world”上面。你会发现,我们传入的字符串也就是我们执行程序后在终端的标准输出上看到的字符串。
这种“所见即所得”得益于 Go 源码文件本身采用的是 Unicode 字符集,而且用的是 UTF-8 标准的字符编码方式,这与编译后的程序所运行的环境所使用的字符集和字符编码方式是一致的。
Go 语言的正式语法规范是使用**分号“;”**来做结尾标识符的。
那为什么我们很少在 Go 代码中使用和看到分号呢?这是因为,大多数分号都是可选的,常常被省略,不过在源码编译时,Go 编译器会自动插入这些被省略的分号。
# 4.3 go程序编译过程
Go 是一种编译型语言,这意味着只有你编译完 Go 程序之后,才可以将生成的可执行文件交付于其他人,并运行在没有安装 Go 的环境中。
Go 源码需要先编译,再分发和运行。如果是单 Go 源文件的情况,我们可以直接使用 go build 命令 +Go 源文件名的方式编译。不过,对于复杂的 Go 项目,我们需要在 Go Module 的帮助下完成项目的构建。
go build 编译成功后,会获得一个二进制的可执行文件
$go build main.go
$ls
main* main.go
2
3
go run 基于源码文件的直接执行
$go run main.go
hello, world
2
当然像 go run 这类命令更多用于开发调试阶段,真正的交付成果还是需要使用 go build 命令构建的。
# 4.3.1 复杂项目下 Go 程序的编译
Go module 构建模式是在 Go 1.11 版本正式引入的,为的是彻底解决 Go 项目复杂版本依赖的问题,在 Go 1.16 版本中,Go module 已经成为了 Go 默认的包依赖管理机制和 Go 源码构建机制。
Go Module 的核心是一个名为 go.mod 的文件,在这个文件中存储了这个 module 对第三方依赖的全部信息。
# go mod init
添加 go.mod 文件:
$go mod init github.com/bigwhite/hellomodule
go: creating new go.mod: module github.com/bigwhite/hellomodule
go: to add module requirements and sums:
go mod tidy
2
3
4
go mod init 命令的执行结果是在当前目录下生成了一个 go.mod 文件:
$cat go.mod
module github.com/bigwhite/hellomodule
go 1.16
2
3
4
一个 module 就是一个包的集合,这些包和 module 一起打版本、发布和分发。go.mod 所在的目录被我们称为它声明的 module 的根目录。
// 第一行内容是用于声明 module 路径(module path)的。而且,module 隐含了一个命名空间的概念,module 下每个包的导入路径都是由 module path 和包所在子目录的名字结合在一起构成。
module github.com/bigwhite/hellomodule
// 最后一行是一个 Go 版本指示符,用于表示这个 module 是在某个特定的 Go 版本的 module 语义的基础上编写的。
go 1.16
2
3
4
# go mod tidy
自动检测、添加go依赖包:
$go mod tidy
go: downloading go.uber.org/zap v1.18.1
go: downloading github.com/valyala/fasthttp v1.28.0
go: downloading github.com/andybalholm/brotli v1.0.2
... ...
2
3
4
5
从输出结果中,我们看到 Go 工具不仅下载并添加了 hellomodule 直接依赖的 zap 和 fasthttp 包的信息,还下载了这两个包的相关依赖包。
go mod tidy 执行后,go.mod 的最新内容变成了这个样子:
module github.com/bigwhite/hellomodule
go 1.16
require (
github.com/valyala/fasthttp v1.28.0
go.uber.org/zap v1.18.1
)
2
3
4
5
6
7
8
go.mod 已经记录了 hellomodule 直接依赖的包的信息。不仅如此,hellomodule 目录下还多了一个名为 go.sum 的文件,这个文件记录了 hellomodule 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。在构建的时候,如果本地依赖包的 hash 值与 go.sum 文件中记录的不一致,就会被拒绝构建。