第 1 课 · Hello, World 与一个 Go 程序的诞生

学习路线

教材对应:《Go程序设计语言》第 1 章 入门

环境:Go 1.22.2 / windows-amd64


0. 先建立一个核心认知

Go 是编译型语言,但它自带一个"迷你操作系统"住进你的程序里。

这句话是你后面理解一切"为什么"的钥匙。

  • Python 写的 print("hello") 需要你机器上装了 Python 解释器才能跑;
  • Go 写的 fmt.Println("hello") 编译完是一个独立的 .exe,丢到任何一台同架构的 Windows 上就能跑,什么都不用装。

误区

a. 错误理解:直接在powershell中运行


b. 正确理解:先在开发机写 .go 源码,用 Go 编译出 .exe;然后把 .exe 放到另一台同架构的 Windows 上直接运行,目标机不用装 Go。开发机仍然需要装 Go 才能编译。

代价是什么?往下看第 4 节


1. Go 是什么:三股血缘(书 p.5--7)

书的前言部分讲了 Go 的"家谱",这不是闲聊,它直接解释了 Go 为什么长这样:

血缘 继承了什么 你现在能看到的地方
C 语言 表达式语法、控制流、基础类型、指针、传值调用、追求机器码效率 if / for / int / *&
Pascal → Modula-2 → Oberon 包的概念、导入/声明语法、方法声明语法 import "fmt"package main、方法写在类型外面
CSP(Tony Hoare, 1978) "用通信来共享内存,而不是用共享内存来通信" goroutine + channel(第 8 章,全书灵魂)

这张表的意思是:

a. C 语言:给了 Go 表达式、控制流、基础类型、指针、传值、追求效率

b. Pascal/Modula-2/Oberon:给了 Go 包、导入、声明语法、方法写在类型外面、极简模块化。

c. CSP:给了 Go goroutine + channel 的并发哲学

C 给了 Go 身体,Oberon 给了 Go 骨架,CSP 给了 Go 灵魂。

还有两个 Go 自己发明的东西,别的语言抄不走的:

  • slice(切片):动态数组 + 底层共享内存,兼顾性能和灵活
  • defer 语句:延迟执行,Go 里资源释放的标准姿势

补充:书上这句「用通信来共享内存」是整本书的题眼。现在记住就行,后续拆开讲透。面试被问"Go 并发和其他语言有什么本质区别",答这一句就对了。


2. 环境:建你的第一个 Go 项目

2.1 四个环境变量,别再混淆

复制代码
# 真实地址隐藏,用x/X代替
GOROOT  = D:\xxxxxxxxx        Go 自己装在哪(编译器、标准库源码在这)
GOPATH  = F:\XXXXXXXXXXX       你的工作区(下载的第三方包缓存在这)
GOMODCACHE = XXXXXXXXX    模块缓存的真正位置
GOPROXY = https://goproxy.cn      下载第三方包走哪个源

GOPROXY 改成了 goproxy.cn(原本是 proxy.golang.org,国内会卡)。

配置文件在 C:\peizhiwenjian,你自己改的话用:

bash 复制代码
go env -w GOPROXY=https://goproxy.cn,direct

a. 配置文件里的值:可以是网址、本地路径、数字、开关
b. Go 的用户级配置文件在这个本地文件里:C:\peizhiwenjian
c. 你自己改,意思是往这个本地文件里写一条配置:GOPROXY=https://goproxy.cn,direct,相当于在本地配置文件里,给 GOPROXY 这个格子填了一个网址
d. GOPROXY 作用:Go 在下载第三方模块时,需要知道去哪里下载。

2.2 go.mod:现代 Go 项目的身份证

bash 复制代码
# 创建(mkdir )一个名为 go-study 的目录
# 执行成功后,进入这个目录
mkdir go-study && cd go-study
# 把当前目录初始化成一个 Go 模块,模块名是: go-study
go mod init go-study

会生成一个 go.mod

复制代码
# go.mod 的内容
module go-study

go 1.22

module go-study

这是模块路径 / 模块名。

它表示:这个模块的导入路径前缀是 go-study。

⚠️ 书与现实的第 1 处代差

书上讲的是 GOPATH 模式:所有代码必须放在 $GOPATH/src/你的项目/ 下面,否则编译不过。

**这套东西在 Go 1.16 之后已经被废弃了。**现在是 Go Modules 时代:项目可以放在磁盘任何位置,只要有 go.mod 就是一个独立项目。


3. Hello, World 逐行拆解(书 p.13)

go 复制代码
package main

import "fmt"

func main() {
	fmt.Println("Hello, 世界")
}

四行代码,每一行都不能改,我逐行讲:

第 1 行 package main

  • package包声明,必须写在文件第一行(注释除外)。
  • Go 里没有"类"和"命名空间",包就是唯一的组织单位
  • main 这个包名是特殊的:它告诉编译器"这玩意儿要编译成可执行文件,不是库"。
    • 你写成 package myappgo build 会成功但不产生 .exe------它编出来的是个库。

第 2 行 import "fmt"

  • 导入标准库的 fmt 包(format 的缩写,负责格式化输入输出)。
  • **import 了就必须用,不用就编译报错。**这是 Go 的硬性规定,不是警告。
    • 为什么这么狠?因为 Go 的设计哲学是"编译快",不用的依赖会被立刻清掉,避免像 C++ 那样 include 一堆头文件导致编译半小时。
  • 多个包的标准写法:
go 复制代码
import (
    "fmt"
    "os"
)

第 3 行 func main() {

  • func 声明函数;main 是入口函数名。
  • main 函数必须满足三个条件 :在 main 包里、名字叫 main、无参数无返回值。少一个都编译不过。
  • 开头的 { 必须和 func 在同一行------原因见第 6 节,这是新手第一大坑。

第 4 行 fmt.Println("Hello, 世界")

  • fmt 是包名,Println 是该包导出的函数(大写字母开头 = 对外可见,小写则是包内私有)。
  • Go 的源文件默认是 UTF-8,所以字符串里直接写中文完全没问题。
  • 注意:Go 的字符串不是"一堆字符",而是一段不可变的字节序列 。所以 "Hello, 世界" 的字节长度是 13 而不是 9("世界"每个汉字 3 个字节,UTF-8 编码)。
    • 这个点在第 3 章(p.77)会展开,现在先埋个种子。

没有分号?

Go 语句末尾不写分号。但不是没有,是编译器帮你插了------见第 6 节。


4. 底层:从 hello.gohello.exe 发生了什么

4.1 编译的五个阶段

阶段 做什么 产物
1. 词法分析 把源码切成 token(标识符、关键字、字面量) token 流
2. 语法分析 按语法规则组装成树 抽象语法树 AST
3. 类型检查 检查类型是否匹配,做逃逸分析 带类型的 AST
4. 中间代码 + 优化 转成 SSA 形式,做内联、死代码消除、边界检查消除 优化后的中间码
5. 生成机器码 + 链接 编译成 amd64 机器码,和 runtime 静态链接 .exe

想看第 4 阶段编译器做了哪些优化,可以加参数:

bash 复制代码
go build -gcflags="-m" hello.go     # 打印内联和逃逸分析决策
bash 复制代码
acer@DESKTOP-SDPT4C3 MINGW64 /f/go/clara-pra
$ go build -gcflags="-m" hello.go
# command-line-arguments
./hello.go:5:6: can inline main
./hello.go:6:13: inlining call to fmt.Println
./hello.go:6:13: ... argument does not escape
./hello.go:6:14: "Hello, World!" escapes to heap

4.2 实测:为什么打印一行字要 1.9 MB?

构建方式 文件大小
默认 go build 1,988,608 字节(1.90 MB)
去掉符号表和调试信息 -ldflags="-s -w" 1,332,224 字节(1.27 MB)
再关闭内联 -gcflags="-l" 1,332,736 字节

1 MB = 1024 KB

1942 KB ÷ 1024 ≈ 1.8965 MB ≈ 1.90 MB
go build -ldflags="-s -w" -o hello.exe hello.go
go build -ldflags="-s -w" -gcflags="-l" -o hello.exe hello.go

结论拆开看:

  1. 符号表 + DWARF 调试信息占了 656 KB(33%) 。这部分只是给调试器用的,生产环境用 -s -w 砍掉,体积立减三分之一。这是 Go 服务发布的标准操作。
  2. 剩下的 1.27 MB 几乎全是 Go runtime。你的代码只有几行,但 runtime 被静态链接进来了。

4.3 用符号表证明这件事

-ldflags="-s -w"

由于之前执行上述命令,去掉了符号表的版本,所以结果是这样的


重新编一个默认版本

go build -o hello_default.exe hello.go

bash 复制代码
go tool nm hello.exe | wc -l                      # 总共 2063 个符号

按包统计符号数量,排前几名的是:

符号数 说明
runtime. 1413 调度器、GC、内存分配器、栈管理
reflect. 86 反射(因为 fmt 用了反射)
syscall. 81 系统调用封装
sync. 47 锁和原子操作
fmt. 44 你真正 import 的那个包
main. 1 你写的代码

你写的全部代码,在这个 1.9 MB 的 exe 里只占 1 个符号。

那 1413 个 runtime 符号里包括:

复制代码
T runtime.mallocgc    内存分配器(你每次 make/map/切片扩容 都走它)
T runtime.gcStart     垃圾回收的启动入口
T runtime.newproc     你写 go func(){} 时真正调用的东西
T runtime.schedule    goroutine 调度器的主循环

这就是 Go 的取舍 :用体积换"零依赖部署 + 内置并发 + 自动 GC"。

一个 Go 的 Docker 镜像可以做到只有几 MB,因为里面除了这个静态二进制什么都不用装。


5. 底层:执行go tool nm hello.exe之后,第一个执行的代码不是你的 main

在符号表里能看到两个"main":

复制代码
437100 T runtime.main    ← 真正的入口
489520 T main.main       ← 你写的

程序启动的真实顺序是:

复制代码
操作系统加载 PE 文件
    ↓
_rt0_amd64_windows        (汇编,设置栈)
    ↓
runtime·rt0_go            (初始化 m0/g0,即主线程和第一个 goroutine 的栈)
    ↓
runtime·schedinit         (初始化调度器、内存分配器、GC、创建 P)
    ↓
runtime·newproc           (创建一个 goroutine 去跑 runtime.main)
    ↓
runtime·mstart            (启动调度循环,让出 CPU)
    ↓
runtime.main              (Go 层面的入口)
    ├─ 启动 GC 后台清扫 goroutine
    ├─ 执行所有包的 init() 函数
    └─ **调用 main.main**  ← 你的代码在这里才第一次被执行
    ↓
main.main 返回 → runtime.exit(0)

关键认知 :你的 main.main 是跑在一个 goroutine 里的。Go 程序从启动那一刻起就是并发的,哪怕你一行 go 都没写。

这点书上完全没讲,但理解了它,第 8、9 章的很多东西会突然变得自然。


6. Go 的隐藏规则:自动分号插入(新手第一大坑)

规则 :Go 编译器在扫描时,如果一行的末尾是下面这些东西之一,就自动插入一个分号

复制代码
标识符(变量名、函数名)、字面量(数字、字符串)
break  continue  fallthrough  return
++  --  )  ]  }

所以 func main() 这一行末尾是 ),编译器实际看到的是 func main();

推论:{ 绝对不能另起一行。

我实测了一下,把大括号换行:

go 复制代码
func main()
{
    fmt.Println("brace on new line")
}

编译器报:

复制代码
demo\bad_brace.go:6:1: syntax error: unexpected semicolon or newline before {

因为编译器看到的是:

go 复制代码
func main();      // ← 分号被自动插在这里,函数体没了
{                 // ← 这个 { 成了孤魂野鬼

这个坑影响所有带大括号的语句ifforswitchfunc 全都要遵守。

好消息是 gofmt 会自动帮你修正,IDE 保存时自动格式化就永远不会遇到。


7. go run / go build / go install 到底差在哪

命令 做什么 产物 什么时候用
go run hello.go 编译到临时目录再立刻执行,跑完删掉 无(看不到文件) 写两行代码快速验证
go build 在当前目录生成可执行文件 hello.exe 本地调试、要拿产物
go install 编译并放到 $GOPATH/bin $GOPATH/bin/hello.exe 想把工具装成全局命令

注意go run 每次都要重新编译,所以启动比 go build 出来的 exe 慢。感觉"go run 好慢"是正常的。


8. gofmt:Go 不讨论代码风格

Go 官方直接把代码格式统一了,gofmt 是标准:

bash 复制代码
gofmt -l .        # 列出格式不合规的文件
gofmt -w .        # 直接改写成规范格式

特点

  • 用 tab 缩进,不是空格。Go 官方就是这么定的。
  • 没有"要不要换行""空格几个"这种争论------gofmt 说了算。
  • 这是 Go 工程文化的一部分:所有 Go 项目代码看起来都像一个人写的。

配好 IDE 的 "Format on Save",这个习惯从第一课就养成。

8.1 vscode中配Format on Save

配好后,你写:

go 复制代码
package main
import "fmt"
func main(){
fmt.Println("hello")
}

按 Ctrl+S,自动变成:

go 复制代码
package main

import "fmt"

func main() {
	fmt.Println("hello")
}

9. 本课小结

问题 答案
package main 为什么特殊 它决定编译出可执行文件而不是库
import 了不用会怎样 编译报错(不是警告)
大写字母开头的标识符意味着什么 对外可见(exported)
为什么 hello world 有 1.9 MB 静态链接了 Go runtime(1413 个符号)
发布时怎么瘦身 go build -ldflags="-s -w",减 33%
{ 能不能换行 不能,自动分号插入会导致语法错误
你的 main 是程序第一个跑的代码吗 不是,是 runtime.main 调用它的

10. 练习题

要求:先自己写,写完再对答案。卡住超过 15 分钟就看提示,别死磕。


练习 1.1(基础)

写一个程序,输出三行:你的名字、你公司的名字、一句你想说的话。

要求:用且只用一个 fmt.Println 调用完成。

提示:查一下 Println 接受几个参数。


练习 1.2(体积验证)

在你自己机器上重现第 4.2 节的实验:

bash 复制代码
go build -o a.exe hello.go
go build -ldflags="-s -w" -o b.exe hello.go

对比两者大小,算出符号表占了多少百分比。

然后思考:如果你的服务有 100 个 Go 微服务要发布,砍掉这 33% 意味着什么?


练习 1.3(符号表探查)

运行 go tool nm a.exe,回答:

  1. 你的程序里有 runtime.gcStart 这个符号吗?它说明什么?
  2. 找找有没有 net/http 相关的符号?为什么?(提示:你 import 了吗)
  3. import "fmt" 换成 import "os" 然后用 os.Exit(0)(不输出任何东西),重新编译看看体积变化。

练习 1.4(自动分号,必做)

故意写一段大括号换行的错误代码,亲眼看一下那个报错。

然后想一想:下面这段代码能编译过吗?为什么?

go 复制代码
x := 1 +
    2

提示:第一行末尾是 +,不在自动插入分号的列表里。


练习 1.5(思考题,不难但要动脑)

Go 的 import 不用就报错。你觉得这个设计:

  • 解决了什么问题?
  • 带来了什么麻烦?(提示:想想调试时临时注释掉一段代码)

练习 1.6(进阶 · 为下节课铺垫)

fmt.Println("Hello, 世界") 输出的这个字符串,它的字节长度字符个数分别是多少?

先自己猜,然后写程序验证:

go 复制代码
s := "Hello, 世界"
fmt.Println(len(s))  // 这个是字节长度

怎么拿到"字符个数"?自己查一下 utf8 包或者 range 遍历。

这题答不出来没关系,它是第 3 章(p.77)的核心内容,下下节课我会讲。


下一课:第 1 章剩下的部分------命令行参数(p.16)、查找重复的行(p.20),以及 Go 里最重要的两个数据结构:slice 和 map。

相关推荐
php@king1 小时前
golang入门到精通
开发语言·后端·golang
Leo.yuan1 小时前
Flink + Kafka + Doris 之外的另一条路:FineDataLink 一体化实时数仓方案
开发语言·javascript·ecmascript
只睡四小时1 小时前
端侧推理实战:face-api.js 零后端人脸识别踩坑记
开发语言·javascript·ecmascript
某不知名網友1 小时前
C++ 深浅拷贝:从默认拷贝到 Rule of Five
java·开发语言
君顾11 小时前
外卖CPS小程序开发实战指南:从零到上线的完整流程
java·开发语言·外卖
明月_清风1 小时前
Foundry Cheatcode 详解:prank、warp、deal、expectRevert、expectEmit
后端·web3·solidity
blue_ice .1 小时前
DDS原理及简易实现
开发语言·经验分享·笔记·嵌入式硬件·fpga开发
励志不掉头发的内向程序员1 小时前
【LibreCAD 2D架构】鼠标点下的坐标为什么会被“吸”走?LibreCAD 对象捕捉系统解析
开发语言·c++·qt·学习·系统架构
鹅城剑仙1 小时前
Spring Boot 3 全局异常处理与统一响应封装进阶实战
java·spring boot·后端