尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

第 1 课 · Hello, World 与一个 Go 程序的诞生 学习路线教材对应《Go程序设计语言》第 1 章 入门环境Go 1.22.2 / windows-amd640. 先建立一个核心认知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、方法写在类型外面CSPTony Hoare, 1978“用通信来共享内存而不是用共享内存来通信”goroutinechannel第 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你自己改的话用goenv-wGOPROXYhttps://goproxy.cn,directa. 配置文件里的值可以是网址、本地路径、数字、开关b. Go 的用户级配置文件在这个本地文件里:C:\peizhiwenjianc. 你自己改意思是往这个本地文件里写一条配置GOPROXYhttps://goproxy.cn,direct相当于在本地配置文件里给 GOPROXY 这个格子填了一个网址d.GOPROXY 作用Go 在下载第三方模块时需要知道去哪里下载。2.2 go.mod现代 Go 项目的身份证# 创建mkdir 一个名为 go-study 的目录# 执行成功后进入这个目录mkdirgo-studycdgo-study# 把当前目录初始化成一个 Go 模块模块名是 go-studygo mod init go-study会生成一个go.mod# go.mod 的内容 module go-study go 1.22module go-study这是模块路径 / 模块名。它表示这个模块的导入路径前缀是 go-study。⚠️ 书与现实的第 1 处代差书上讲的是 GOPATH 模式所有代码必须放在$GOPATH/src/你的项目/下面否则编译不过。**这套东西在 Go 1.16 之后已经被废弃了。**现在是 Go Modules 时代项目可以放在磁盘任何位置只要有go.mod就是一个独立项目。3. Hello, World 逐行拆解书 p.13packagemainimportfmtfuncmain(){fmt.Println(Hello, 世界)}四行代码每一行都不能改我逐行讲第 1 行package mainpackage是包声明必须写在文件第一行注释除外。Go 里没有类和命名空间包就是唯一的组织单位。main这个包名是特殊的它告诉编译器这玩意儿要编译成可执行文件不是库。你写成package myappgo build会成功但不产生 .exe——它编出来的是个库。第 2 行import fmt导入标准库的fmt包format 的缩写负责格式化输入输出。**import 了就必须用不用就编译报错。**这是 Go 的硬性规定不是警告。为什么这么狠因为 Go 的设计哲学是编译快不用的依赖会被立刻清掉避免像 C 那样 include 一堆头文件导致编译半小时。多个包的标准写法import(fmtos)第 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.go到hello.exe发生了什么4.1 编译的五个阶段阶段做什么产物1. 词法分析把源码切成 token标识符、关键字、字面量token 流2. 语法分析按语法规则组装成树抽象语法树 AST3. 类型检查检查类型是否匹配做逃逸分析带类型的 AST4. 中间代码 优化转成 SSA 形式做内联、死代码消除、边界检查消除优化后的中间码5. 生成机器码 链接编译成 amd64 机器码和runtime 静态链接.exe想看第 4 阶段编译器做了哪些优化可以加参数go build-gcflags-mhello.go# 打印内联和逃逸分析决策acerDESKTOP-SDPT4C3 MINGW64 /f/go/clara-pra $ go build-gcflags-mhello.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 heap4.2 实测为什么打印一行字要 1.9 MB构建方式文件大小默认go build1,988,608 字节1.90 MB去掉符号表和调试信息-ldflags-s -w1,332,224 字节1.27 MB再关闭内联-gcflags-l1,332,736 字节1 MB 1024 KB1942 KB ÷ 1024 ≈ 1.8965 MB ≈ 1.90 MBgo build -ldflags-s -w -o hello.exe hello.gogo build -ldflags-s -w -gcflags-l -o hello.exe hello.go结论拆开看符号表 DWARF 调试信息占了 656 KB33%。这部分只是给调试器用的生产环境用-s -w砍掉体积立减三分之一。这是 Go 服务发布的标准操作。剩下的 1.27 MB 几乎全是 Go runtime。你的代码只有几行但 runtime 被静态链接进来了。4.3 用符号表证明这件事-ldflags“-s -w”由于之前执行上述命令去掉了符号表的版本所以结果是这样的重新编一个默认版本go build -o hello_default.exe hello.gogo 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在符号表里能看到两个main437100 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();。推论{绝对不能另起一行。我实测了一下把大括号换行funcmain(){fmt.Println(brace on new line)}编译器报demo\bad_brace.go:6:1: syntax error: unexpected semicolon or newline before {因为编译器看到的是funcmain();// ← 分号被自动插在这里函数体没了{// ← 这个 { 成了孤魂野鬼这个坑影响所有带大括号的语句if、for、switch、func全都要遵守。好消息是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. gofmtGo 不讨论代码风格Go 官方直接把代码格式统一了gofmt是标准gofmt-l.# 列出格式不合规的文件gofmt-w.# 直接改写成规范格式特点用 tab 缩进不是空格。Go 官方就是这么定的。没有要不要换行空格几个这种争论——gofmt说了算。这是 Go 工程文化的一部分所有 Go 项目代码看起来都像一个人写的。配好 IDE 的 “Format on Save”这个习惯从第一课就养成。8.1 vscode中配Format on Save配好后你写packagemainimportfmtfuncmain(){fmt.Println(hello)}按 CtrlS自动变成packagemainimportfmtfuncmain(){fmt.Println(hello)}9. 本课小结问题答案package main为什么特殊它决定编译出可执行文件而不是库import 了不用会怎样编译报错不是警告大写字母开头的标识符意味着什么对外可见exported为什么 hello world 有 1.9 MB静态链接了 Go runtime1413 个符号发布时怎么瘦身go build -ldflags-s -w减 33%{能不能换行不能自动分号插入会导致语法错误你的 main 是程序第一个跑的代码吗不是是runtime.main调用它的10. 练习题要求先自己写写完再对答案。卡住超过 15 分钟就看提示别死磕。练习 1.1基础写一个程序输出三行你的名字、你公司的名字、一句你想说的话。要求用且只用一个fmt.Println调用完成。提示查一下Println接受几个参数。packagemainimportfmtfuncmain(){fmt.Println(Jenna\nCompany\nIm the best!)}PS F:\go\clara-pra\0911prago build-oa.exe 01pra.go PS F:\go\clara-pra\0911prago run .\01pra.go Jenna Company Im the best!练习 1.2体积验证在你自己机器上重现第 4.2 节的实验go build-oa.exe 01pra.go go build-ldflags-s -w-ob.exe 01pra.go对比两者大小算出符号表占了多少百分比。然后思考如果你的服务有 100 个 Go 微服务要发布砍掉这 33% 意味着什么假设每个微服务默认镜像 / 二进制是 20 MB假设单个项目大小 20 MB去符号后约 13.4 MB那么100 个项目总大小 2000 MB去符号后约 1340 MB节省 — 约 660 MB更实际的影响镜像更小如果打进 Docker 镜像拉取、推送、部署都更快。容器启动时加载的数据更少。分发更快CI/CD 上传下载时间减少。内网分发、边缘节点更新更省带宽。存储更省镜像仓库占用减少。备份、归档压力变小。成本更低云厂商按存储、流量计费时直接省钱。节点磁盘压力小能放更多服务。代价生产环境用 -ldflags“-s -w” 的代价是丧失事后用调试器深入分析的能力。不能直接用 go tool nm、objdump 友好地分析。调试版本要单独保留一份带符号的。所以实际做法通常是开发 / 测试默认编译保留符号方便调试。 生产发布-ldflags-s -w减小体积。 同时保留一份带符号的二进制用于事后分析。练习 1.3符号表探查运行go tool nm a.exe回答你的程序里有runtime.gcStart这个符号吗它说明什么找找有没有net/http相关的符号为什么提示你 import 了吗把import fmt换成import os然后用os.Exit(0)不输出任何东西重新编译看看体积变化。runtime.gcStart说明 Go 运行时包含垃圾回收器并且 GC 启动路径被链接进 exe你只打印一行字GC 代码也在里面这是 exe 1.9 MB 的主要原因之一net/http无go tool nm .\a.exe | Select-String “net/http”go tool nm .\a.exe | Select-String http打印结果为空c.exe 会比 a.exe 小一些但不会小太多。去掉 fmt 确实能省掉 fmt、reflect、strconv、internal/fmtsort 等一堆包。 但 Go 运行时本身还在调度器、GC、内存分配器、栈管理、启动代码。 这些才是大头尤其是 runtime 那 1400 多个符号。 os.Exit(0) 本身也依赖 runtime 的一部分但比 fmt.Println 轻。练习 1.4自动分号必做故意写一段大括号换行的错误代码亲眼看一下那个报错。然后想一想下面这段代码能编译过吗为什么x:12提示第一行末尾是不在自动插入分号的列表里。Go 不是“按行结束语句”而是“在特定 token 后自动插入分号”。行尾是 、-、*、/、、||、, 等不会插分号可以继续下一行。行尾是标识符、字面量、)、]、}、return 等会自动插分号语句就结束了。练习 1.5思考题不难但要动脑Go 的import不用就报错。你觉得这个设计解决了什么问题带来了什么麻烦提示想想调试时临时注释掉一段代码解决了什么编译产物干净不打包没用的包依赖图真实不留僵尸依赖显式区分“副作用导入”和“普通导入”编译更快代码始终整洁带来了什么麻烦调试时注释代码要同时改 import临时试代码要来回加删 import重构时容易被打断初学者容易卡在 imported and not used缓解方式用 _ 表示副作用导入用 goimports 或 IDE 自动整理 import配好 Format on Save / Organize Imports临时调试可以用 _ pkg.Something 占位练习 1.6进阶 · 为下节课铺垫fmt.Println(Hello, 世界)输出的这个字符串它的字节长度和字符个数分别是多少先自己猜然后写程序验证s:Hello, 世界fmt.Println(len(s))// 这个是字节长度怎么拿到字符个数自己查一下utf8包或者range遍历。这题答不出来没关系它是第 3 章p.77的核心内容下下节课我会讲。下一课第 1 章剩下的部分——命令行参数p.16、查找重复的行p.20以及 Go 里最重要的两个数据结构slice 和 map。
返回列表