Golang基础篇(1):从 Hello World 到可执行文件——Go 程序结构与编译运行机制
一、前言
通常我们第一个编写的程序 Hello World!
package main
import "fmt"
func main(){
fmt.Println("Hello,World!)
}
执行:go run main.go ,终端便会输出 Hello World!
整个过程非常简单,但有几个地方值得思考
package main和func main()分别承担什么作用?- 为什么 Go 程序需要声明 Package?
- 一个 Go 项目中有多个
.go文件时,它们是如何组织起来的? - 执行
go run时,Go 到底做了什么? - Go 是编译型语言,那么源代码最终是如何变成操作系统可以执行的程序的?
二、Go程序基本结构
拆解Hello World
1、package main:声明包
Go 使用 Package(包)组织代码。
可以把 Package 理解为 Go 代码组织和编译的基本单位。一个目录下属于同一个包的 Go 源文件,通常会被作为一个整体进行编译。
其中,main 是一个特殊的包名。
当一个包声明为 main,并且提供了符合要求的 main 函数时,它可以作为一个独立的可执行程序入口。
需要注意:
package main表示当前文件属于 main 包。main包并不是所有 Go 文件都必须使用的包名。- 普通功能代码通常会放在其他包中,例如 utils
、database、handler`等。
2. import:导入依赖
import 用于导入其他 Package,使当前包能够使用其中公开的标识符
如果需要导入多个包,可以使用:
import (
"fmt"
"strings"
"time"
)
Go 的依赖关系主要通过 Package 之间的导入关系建立。
当一个包导入另一个包时,Go 工具链会根据依赖关系组织编译过程。
3. func main():程序入口
main 函数是 main 包中的特殊函数,也是普通 Go 可执行程序启动后执行的入口。
package main:决定当前包可以构建为可执行程序。func main():提供程序启动后的入口函数。
二者缺一不可。
另外main 函数不接受参数,也不返回值。程序运行时的命令行参数需要通过 os.Args 等方式获取
三、Package、Module 与源文件之间的关系
刚开始学习 Go 时,很容易把 Package、Module 和 .go 文件混为一谈。
实际上,它们是不同层次的概念。
3.1源文件
Go源文件通常以 .go 作为扩展名,是我们编写 Go 代码的载体。
一个 Package 可以由多个 Go 源文件组成,这些文件需要声明属于同一个 Package。
例如:
demo/
├── main.go
└── user.go
这两个源文件开头都声明
package main 都属于同一个包。
在同一个包中,一个源文件可以直接使用另一个源文件中定义的公开或非公开标识符,不需要通过 import 再次导入自己所属的包。
需要注意:Go 是按照包进行编译的,而不是简单地把每一个 .go 文件当作独立程序进行编译。
3.2Package(包)
Package 是 Go 代码组织和编译的基本单位。
同一个目录下的 Go 源文件通常构成一个 Package(忽略测试文件、特殊构建约束等情况)。
例如:
project/
├── main.go
├── user/
│ └── user.go
└── order/
└── order.go
这里包含三个目录:
- 根目录:一个 Package。
- user 目录:一个 Package。
- order 目录:一个 Package。
每个目录中的 Go 文件共同组成对应的包。
不同目录通常对应不同的 Package,即使它们使用相同的包名,也不能因此认为它们是同一个包。
3.3Module(模块)
如果说 Package 是组织代码的基本单位,那么 Module 就是 Go 依赖管理和版本管理的基本单位。
Go Module 是由一组相关 Package 组成的集合。【工程语义上的相关】
一个 Go Module 通常通过根目录下的 go.mod 文件进行定义。
创建一个新项目 mkdir go-demo cd go-demo go mod init go-demo
执行完后生成:
go-demo/
└── go.mod
go.mod 里面包含:
module:声明当前模块的模块路径。go:声明该模块要求的 Go 语言版本。
3.4三者之间的关系
我们可以通过下面的结构理解它们:
Go Module
│
├── go.mod
│
├── main.go
│
├── user/
│ ├── user.go
│ └── profile.go
│
└── order/
└── order.go
对应关系:
| 概念 | 作用 |
|---|---|
| 源文件 | 编写代码的文件 |
| Package | 组织代码、进行编译的基本单位 |
| Module | 组织多个 Package,管理依赖和版本 |
Go 的代码组织并不是简单地按照文件划分,而是通过目录形成 Package,再通过 Module 管理多个 Package。
四、Go 程序是如何从源代码变成可执行文件的?
了解了 Go 程序的基本组织结构之后,我们再来看看 Go 程序的编译过程。
Go 是一门编译型语言。我们编写的 .go 文件本质上是人类可读的源代码,CPU 并不能直接执行这些源代码。因此,需要通过编译工具链,将源代码转换为目标平台能够执行的机器代码,并最终生成可执行文件。
4.1 整体编译流程
Go源代码 --> Go命令加载构建目标 --> 编译器处理 --> 生成目标代码及相关信息 -->
连接器 --> 可执行文件 --> 操作系统加载 --> 程序运行
上述为简化流程,实际 Go 工具链还涉及依赖分析、构建缓存、汇编处理、平台相关处理等环节。
下面分别了解其中几个关键阶段。
4.2 第一阶段:词法分析与语法分析
编译器拿到 Go 源代码之后,首先需要理解代码的结构。
此过程分为两部分:
- 词法分析:将源代码拆分成一个个有意义的Token(词法单元)
- 语法分析:判断Token的排列是否符合Go语言的语法规则
4.3第二阶段:类型检查与中间表示
语法正确,并不代表程序一定正确。
例如:
package main
func main() {
var age int = "18"
}
这段代码在语法层面基本符合 Go 语言的表达形式,但是把字符串赋值给 int 类型变量,违反了 Go 的类型规则。
因此,编译器还需要进行类型检查。
类型检查主要负责检查:
- 变量和表达式的类型是否匹配。
- 函数调用的参数是否符合要求。
- 使用的标识符是否存在。
- 运算操作是否符合类型规则。
通过相关检查后,编译器还会将代码转换为内部使用的中间表示(Intermediate Representation,IR)。
中间表示的作用是让编译器能够以更适合处理和优化的形式描述程序。
为什么需要中间表示?
因为源代码的写法更注重开发者的表达,而编译器需要一种更加统一、便于分析和优化的内部形式。
例如,编译器可以在后续阶段对代码进行优化,减少不必要的计算和操作。
4.4 第三阶段:代码优化与机器码生成
完成前面的处理之后,编译器会进一步处理内部表示,并生成目标平台对应的机器代码。
此处涉及两个重要概念:
1. 代码优化
编译器会尝试对程序进行优化,例如:
- 消除无用代码。
- 删除不必要的检查。
- 优化表达式。
- 优化部分控制流。
优化的目的通常是在不改变程序可观察行为的前提下,让生成的代码更加高效。
需要注意,编译器优化不意味着所有代码都会变得更快,具体效果取决于程序本身和编译器的优化策略。
2. 机器码生成
Go 编译器会根据目标平台生成对应的机器代码。
例如:
go env GOOS
go env GOARCH
可以查看当前 Go 工具链的目标操作系统和 CPU 架构。
常见组合:
| GOOS | GOARCH | 目标平台 |
|---|---|---|
| windows | amd64 | Windows x86-64 |
| linux | amd64 | Linux x86-64 |
| linux | arm64 | Linux ARM64 |
| darwin | arm64 | macOS ARM64 |
这也解释了为什么同一份 Go 源代码可以通过交叉编译生成不同平台的程序。
4.5 第四阶段:链接
编译器生成的目标代码还需要经过链接,才能形成最终的可执行文件。
为什么需要链接?
因为一个 Go 程序往往不只包含自己编写的代码,还会依赖:
- Go 标准库。
- 第三方 Package。
- Go Runtime 运行时支持代码。
这些代码在构建过程中需要被组织起来。
链接器负责处理目标文件之间的符号引用关系,并将程序所需的代码和相关信息组织起来,生成最终的可执行文件。
需要注意:Go 使用静态链接相关机制组织程序代码,但不能简单地认为所有 Go 程序都是完全静态链接。实际结果还会受到 CGO、外部库和构建配置等因素影响。
【注解:
1、CGO是 Go 工具链自带的、用来打通 Go 和 C 的互调用机制(FFI),让 Go 可以调用 C 代码、C 也可以调用 Go
2、静态链接 是 编译阶段,把程序需要用到的库代码,直接复制打包进最终可执行文件里面
3、动态链接 是 程序运行时 不把库代码打包进去,只留下引用;启动时操作系统加载外部动态库】
五、 go run 和 go build 有什么区别?
了解编译过程之后,我们再回到日常开发中最常用的两个命令。
5.1 go run : 编译并运行
执行:
go run main.go
Go 工具会完成构建所需的工作,生成临时可执行程序并运行它,适合日常开发时快速验证代码。
go run 并不是直接解释执行 Go 源代码。
Go 仍然需要经过编译,只是生成的可执行文件由工具链临时管理,而不是作为普通构建产物保留在当前目录中。
5.2 go build:构建可执行文件
执行:
go build
如果当前目录是一个包含 main 包的可执行程序包,Go 会在当前目录生成对应平台的可执行文件。
例如:
go-demo/
├── go.mod
├── main.go
└── go-demo.exe
在 Windows 下通常生成 .exe 文件。
也可以指定输出文件名称:
go build -o app
这样就可以指定生成的可执行文件名称。
5.3 go run 与 go build 对比
| 对比项 | go run | go build |
|---|---|---|
| 是否编译 | 是 | 是 |
| 是否运行程序 | 是 | 否 |
| 是否生成当前目录下的持久化可执行文件 | 默认不会 | 通常会 |
| 主要用途 | 开发调试 | 构建程序 |
| 是否适合部署 | 不作为常规部署方式 | 是 |
这里还有一个值得了解的命令:
go install
它与 go build 不同,除了构建之外,还会将可执行程序安装到 Go 工具链规定的位置。
它会做两件事:
- 编译构建:把目标包编译、链接;
- 安装:把编译出来的二进制可执行文件,放到
GOPATH/bin(或者GOBIN)目录。
因此,日常开发中可以这样理解:
go run:快速编译并运行。go build:构建程序。go install:构建并安装程序。
七、总结
通过本文,我们从一个最简单的 Hello World 程序出发,了解了 Go 程序的基本组成、代码组织方式,以及从源代码到可执行文件的主要过程。
回顾一下:
- 程序结构:
package用于声明包,import用于导入依赖,func main()是可执行程序的入口。 - 代码组织: 源文件组成 Package,多个 Package 可以组织在一个 Module 中。
- 编译过程: Go 工具链会加载构建目标,编译器进行语法分析、类型检查、内部表示转换、优化和机器码生成,最后通过链接器生成可执行文件。
- 常用命令:
go run用于编译并运行,go build用于构建,go install用于构建并安装。
理解程序是如何组织起来的,以及代码最终是如何变成能够运行的程序的,才能逐渐建立起对这门语言的整体认识。

浙公网安备 33010602011771号