简介:Go语言学习笔记PDF共174页,是一份面向Go语言初学者与进阶者的系统化学习资料,内容编排深入浅出,适合从零搭建知识体系或作为日常语法速查手册。笔记分为Go语言基础、标准库、扩展库三大部分,从基础语法到进阶实践均有涉及,涵盖变量与基本类型、函数与闭包、数组与Map、Structs、接口、Goroutine并发、Channel通信、包与测试,以及io、strings、net、sync、os等常用标准库和mgo、godis、snappy等实用扩展库,并对变量零值、类型转换、多返回值、defer/panic/recover、接口类型推断、内存布局、反射、cgo、跨平台编译等关键内容给出简明示例与讲解。资源共1个PDF文件,压缩包大小仅1.18MB,便于离线阅读与快速检索,目录结构清晰、知识点颗粒度细,可配合实际编码边学边练。该笔记已在CSDN获得4044人学习,既能帮助新手快速构建Go语言知识框架,也能为开发者提供随手可查的标准库与进阶特性参考。
1. 把174页Go语言学习笔记读成可运行语法,而不是背词典
我见过很多把Go学习笔记当词典用的工程师:从切片追加到接口断言,174页全部抄着标准库注释,等真到改业务代码时,连 append 之后容量翻了几倍都说不准。其实读一份 Go语言学习笔记.pdf 的正确顺序,不是按页码背语法,而是倒过来走:先搭最小的可运行工程,让页面上的代码全部活一遍,再把出错的地方标回原文。这篇文章就把这条主线摊开:从 go mod 工程起手,落到切片 append 扩容与“2006-01-02”时间布局这两个极易跳过而遇坑的节点上,最后用接口设计与 go test 基准来校验这些知识点是否真正迁移到了你的编辑器里。适合已经会任意一门语言、想快速沉淀 Go 语感的开发者,也适合拿这份笔记做复习提纲的人。
2. 用 go mod 搭建 go语言入门的最小可复现工程:环境变量与 go run 启动顺序
很多 go语言入门 资料上来就让你写 hello world,却跳过了工程化这一层。你在笔记上看到的每一个代码块,只有放进一个能反复重建的 module 里,才能确认它跟当前 Go 版本行为一致。我最常用的做法是把GOPATH与模块路径先固定下来,再跑第一个 main。
2.1 用 go env -w 固化环境,让 GOPATH 在各机器上保持一致
Go 的工具链把缓存、编译产物和远端模块都放在以GOPATH为根的一个目录树里,默认值在 Linux 下是$HOME/go,在 macOS 下也是$HOME/go。为了不让笔记本里的示例在不同机器上产生互不相同的目录结构,可以用go env -w写配置:
go env -w GOPATH="$HOME/go" go env -w GOBIN="$HOME/go/bin" export PATH="$HOME/go/bin:$PATH" go version-w会把这些设置持久化到 Go 的环境配置文件里,而不是只对当前终端生效;GOBIN指定go install生成的可执行文件目录。你只要把PATH配一次,后续打开新终端就不需要再重复 export。执行go version是为了先确认工具链可用,常见的报错是go: command not found,这属于 PATH 没配上,与 Go 本体无关。
这里要留意:go env -w只负责写全局变量,不同版本共用一个配置目录,升级 Go 后要重新检查go env GOPATH是否指向了你预期的路径。
2.2 go mod init 把页码概念映射为模块路径
笔记里讲包与导入时,例子往往是import "fmt"这类标准库。换成自己写代码,需要先为当前项目声明模块路径。模块路径会直接出现在 import 语句里,所以建议用能体现项目归属的命名:
mkdir -p ~/learngo-basic && cd ~/learngo-basic go mod init golang-study/notes cat go.mod执行后生成的go.mod第一行是module golang-study/notes,再往下是go 1.xx的版本声明。模块路径不必是真实域名,内网项目也可以用internal/learngo这种相对风格,但要保证它在未来不需要频繁改动,因为一旦在代码里 import 了它,改名就要全局替换。
go mod init还会影响依赖解析:当你 import 第三方包时,go 命令会基于模块路径查找远端仓库。对于学习笔记里的标准库示例,不下载任何第三方依赖也能跑通;若你开始引入外部包,则要确保go env里没有残留旧的模块缓存配置。
2.3 从 go run 到 go vet,编译错误先看哪个关键词
最小可复现工程最终要落到一个可执行入口。在模块根目录下建main.go:
package main import "fmt" func main() { name := "go" fmt.Printf("hello %s\n", name) }然后运行:
go run main.go go build -o learbasic .go run会把源码编译到临时目录后立即执行,适合日常练习;go build生成二进制文件,适合验证跨平台编译和交付。两者相同之处是都以package main为起点,如果文件里没有func main,会报function main is undeclared in the main package这样的错误。
多文件场景下我建议先把所有.go文件放在同一个目录,再执行go run .。遇到编译错误时,先看错误文本里undefined:之后的标识符,多数情况是函数没导出或包没导入;再看cannot use部分,这通常指类型不匹配。若想提前拦截这类问题,可以运行go vet,它会对代码做静态检查,比编译错误给出更前置的提示。
3. 从 append go语言 这个高频问题展开:切片共享、扩容与容量边界
在 Go 里,append大概是搜索引擎上出现频次最高的内置函数之一。它明明只有两三个参数,却引申出“底层数组什么时候变”“为什么子切片改了会影响原切片”等一连串问题。理解透这一章,笔记里切片相关页码基本可以整章划掉。
3.1 切片头里藏着三件事:指针、长度、容量
切片不是数组,它只是数组视图。运行时里一个切片实例包含三个字段:指向底层数组的指针、长度 len、容量 cap。当把切片赋给另一个变量时,三个字段会被复制,但指针仍指向同一块内存:
package main import "fmt" func main() { base := []int{1, 2, 3, 4, 5} sub := base[1:3] sub[0] = 99 fmt.Println(base) // [1 99 3 4 5] }base[1:3]创建了一个长度为 2、容量为 4 的切片,它的第 0 个元素对应base的第 1 个元素。把sub[0]改成 99 后,base也跟着变,因为它们是同一个底层数组的不同偏移视角。
如果想让子切片独立,常见做法是先 make 一个新切片再 copy。copy(sub, base[1:3])会按目标切片长度复制,源超出部分不影响。这个差异在导出函数返回值时尤其重要:返回出去的切片被调用方改动,内部数组可能被意外污染。
3.2 预分配 cap,快速看懂 append go语言 的扩容规律
append的行为可以分成两个分支:当len(s) < cap(s)时,直接写入当前底层数组并返回原切片,不会发生复制;当len(s) == cap(s)时,分配一块更大的底层数组,把旧元素复制过去,然后返回指向新数组的切片。这就是为什么 append 的返回值必须重新赋值给原变量。
package main import "fmt" func main() { s := make([]int, 0, 2) for i := 1; i <= 6; i++ { s = append(s, i) fmt.Printf("len=%d cap=%d addr=%p\n", len(s), cap(s), s) } }用make([]int, 0, 2)创建容量为 2 的空切片,每轮追加后观察 len 和 cap:
| 追加次数 | 追加后 len | 追加后 cap | 是否更换底层数组 |
|---|---|---|---|
| 1 | 1 | 2 | 否 |
| 2 | 2 | 2 | 否 |
| 3 | 3 | 4 | 是 |
| 4 | 4 | 4 | 否 |
| 5 | 5 | 6 | 是 |
| 6 | 6 | 6 | 否 |
从第 3 次开始,cap 从 2 变为 4;第 5 次又从 4 变为 6。Go 的扩容策略在小容量区间接近翻倍,但并非严格翻倍,容量较大时会转入约 1.25 倍增长并做内存对齐。所以笔记里如果写了“append 一定翻倍扩容”这种话,建议划掉,改成“小容量阶段近似翻倍,具体由运行时决定”。
预分配的意义在于减少复制次数:如果你明确知道要追加 1000 个元素,直接make([]int, 0, 1000),扩容次数可以从 10 次左右降到 0 次。对性能敏感的批量数据处理,这一个改动比很多微优化都直接。
3.3 追加穿过子切片边界,会覆盖父切片的相邻元素
切片共享底层数组带来的最隐蔽问题是:对子切片执行 append 时,只要没超过 cap,就会越过子切片的 len,写入父切片尚未暴露的部分:
package main import "fmt" func main() { parent := []int{1, 2, 3, 4} child := parent[1:3] // [2 3],cap 为 3 child = append(child, 100) // 不扩容,直接写 parent[3] fmt.Println(parent) // [1 2 3 100] }parent[1:3]的 cap 等于parent的 cap 减去起始偏移 1,即 3。向 child 追加一个元素没有触发扩容,于是 100 写进了parent[3]。修复方式是用全切片表达式限制容量:child := parent[1:3:3]。这个语法第三个冒号后的数字规定容量上限为3-1=2,一旦 append 超出 2,就必须分配新数组,不再触碰 parent。
实际业务里,这种错误常在拆分列表时出现:函数接收子切片后 append,结果改写外部数组。遇到这类问题,先看切片的 cap 是否大于 len,再看有没有用三索引切片限制共享范围。
4. go语言中时间为什么是20060102:布局常量、Format 与 Parse 时区
Go 时间格式化是新手第一个容易拍桌子的点。别的语言用YYYY-MM-DD HH:mm:ss,Go 却写2006-01-02 15:04:05。标题里这行“go语言中时间为什么是20060102”拆开看,其实就三件事:参考时间从哪来、布局里的数字怎么记、Parse 与时区怎么配合。
4.1 参考时间不是巧合,它是一个升序的 1 到 7
Go 的 time 包把格式化布局建立在固定参考时间上,这个参考时间写作Mon Jan 2 15:04:05 MST 2006。把它从后往前读,数字顺序正好是年、月、日、时、分、秒、星期、时区缩写:
| 位置 | 参考值 | 对应字段 |
|---|---|---|
| 年 | 2006 | 四位年份 |
| 月 | 01 | 月份 |
| 日 | 02 | 日期 |
| 时 | 15 | 24 小时制小时 |
| 分 | 04 | 分钟 |
| 秒 | 05 | 秒 |
| 星期 | Mon | 周几缩写 |
| 时区 | MST | 时区缩写 |
这种设计让布局本身就是输出样例:"2006-01-02"直接对应“年-月-日”,完全不需要查%Y或yyyy这类符号表。一旦接受了“布局就是参考时间的局部变形”,你就不会再忘记2006出现在哪里。
4.2 用 Format 写出业务时间串:常用布局和两位年陷阱
日常接口对接最常用的是标准日志格式和纯日期格式:
package main import ( "fmt" "time" ) func main() { now := time.Now() fmt.Println(now.Format("2006-01-02 15:04:05")) fmt.Println(now.Format("2006/01/02")) fmt.Println(now.Format("15:04")) }布局里的数字是相对参考时间一一对应的,不是可替换通配符。把年份写成06并不是“两位年份”的简写,而是会把年份按参考时间的偏移解析成0006,导致输出变成06-01-02 15:04:05这样的怪值。若确实需要两位年,比如某些老系统协议,要写成"06-01-02",但请先明确它代表的是 2006 年的偏移,而不是当前年份后两位。
一个常见的误区是混淆 12 小时制与 24 小时制:布局里的15表示 24 小时制,但如果你写"3:04PM",则是 12 小时制加 PM 后缀。两种模式不能混用,"15:04PM"会输出无法预期的结果。
4.3 Parse 与 ParseInLocation:字符串到时间的时区分水岭
格式化是时间转字符串,解析则是反方向。最容易出错的是时区处理:
package main import ( "fmt" "time" ) func main() { t1, _ := time.Parse("2006-01-02 15:04:05", "2025-03-10 08:00:00") t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2025-03-10 08:00:00", time.Local) fmt.Println(t1.Unix()) fmt.Println(t2.Unix()) }time.Parse默认解析为 UTC,t1的 Unix 时间戳会与北京时间相差 8 小时;time.ParseInLocation则按第二个参数指定的地点解释。在业务代码里,用户输入“08:00:00”通常指本地时间,应该用后者。反过来,当你需要把时间按固定时区展示给用户时,用time.LoadLocation("Asia/Shanghai")再结合In方法转换,比手工加减小时更可靠。
还有一点:解析字符串中的时区缩写MST并不等于三字母时区码,它只会被识别为与当前系统时区偏移相关的占位。真正稳定的做法是解析带Z07:00这类 RFC3339 格式,再用时间对象做转换。
5. 用接口设计与 go test 基准验证笔记里的知识点是否内化
到了收尾阶段,与其再往笔记里加页码,不如做两个验证动作。第一个是写一个十几行的接口示例,第二个是跑一次go test基准,两者都能快速暴露你到底是记住语法,还是能用它推导新问题。
5.1 用具名结构体实现接口,检验你对“即插即用”的掌握
Go 的接口是隐式实现:结构体不需要写implements关键字,只要方法集合匹配即可。这种设计在笔记里经常被一句话带过,可一旦落到函数参数上,就会体现出它与 Java 式接口的差异:
package main import "fmt" type Printer interface { Print() string } type Doc struct { title string } func (d Doc) Print() string { return d.title } func PrintAll(items []Printer) { for _, item := range items { fmt.Println(item.Print()) } } func main() { docs := []Printer{Doc{title: "gin"}, Doc{title: "grpc"}} PrintAll(docs) }如果看一眼就能说出“Doc不需要显式声明实现Printer,因为它的方法集刚好匹配”,那这条已经过关。这里的反直觉点在于:PrintAll的参数类型是[]Printer,不能直接传[]Doc,这会跟很多跨语言经验打架。切片类型是逆变还是协变,在 Go 这里答案是不支持隐式转换,必须先手工转。笔记里没写到的这些边界,才是真正值得你往回翻页码的地方。
5.2 用 go test -bench 给时间格式化做一次成本测量
最后一个验证是给知识点套上性能视角:
go test -bench=BenchmarkFormat -run=^$ -benchmem .配合一个简单的测试文件,验证time.Now().Format("2006-01-02 15:04:05")单次调用大概消耗多少内存。-run=^$跳过普通测试,只跑基准;-benchmem会打印每次调用分配的字节数和分配次数。如果日志里显示多次调用发生大量内存分配,说明你的业务循环里可能反复创建时间对象,这时可以考虑复用布局常量字符串,或把格式化结果缓存到结构体字段中。
把笔记读到这一步,真正留下来的不是 174 页原文,而是你能在需要时把知识点转化为可执行判断的能力。下一次遇到append改动了父切片、时间解析差了 8 小时,直接在项目里跑一段最小复现,比翻 PDF 更快得出结论。
本文还有配套的精品资源,点击获取