十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Go 语言学习笔记

Go 语言学习笔记 Go 语言学习笔记一、基础语法1.1 结构体与接口Go 中结构体和接口通过**隐式实现Duck Typing**关联不需要implements关键字。typeSpeakerinterface{Speak()string}typeDogstruct{Namestring}func(d Dog)Speak()string{returnd.Name: Woof!}// Dog 自动满足 Speaker 接口无需声明funcMakeItSpeak(s Speaker){fmt.Println(s.Speak())}方法集规则T值只能调用值接收者方法*T指针可调用值指针接收者方法编译期验证var_SpeakerDog{}// 验证值接收者var_Speaker(*Dog)(nil)// 验证指针接收者1.2 不同接口同名方法场景结果同名同签名一次实现同时满足两个接口同名同签名语义不同编译通过但逻辑错误需拆分类型同名不同签名无法同时满足编译错误Go 只看方法签名不看接口名字或语义。1.3 切片Slice底层结构typeslicestruct{ptr*array// 指向底层数组的指针lenint// 当前长度capint// 容量}len vs caplen当前可读写的元素个数s[i]中0 i len合法cap当前不重新分配底层数组最多能装多少个元素底层数组: [1] [2] [3] [4] [5] [6] [7] [8] ↑ ↑ ↑ ptr len cap[0, len)范围可读可写[len, cap)范围空间已分配但无法直接访问只能通过append逐个扩展超过 cap自动扩容不是硬限制扩容规则Go 1.18条件最终容量期望容量 旧容量 * 2直接用期望容量旧容量 256新容量 旧容量 * 2旧容量 256新容量 旧容量 旧容量/4 192平滑增长趋近 ~1.25x计算后还会进行内存对齐实际 cap 可能更大。扩容本质分配新底层数组 memmove 批量拷贝旧数据。常见陷阱// 1. 共享底层数组a:[]int{1,2,3,4,5}b:a[1:3]bappend(b,99)// cap够不扩容覆盖 a[3]// 2. 三索引切割限制 capb:a[1:3:3]// len2, cap2限制 capbappend(b,99)// cap不够扩容不影响 a排序sort.Slice推荐sort.Slice(people,func(i,jint)bool{returnpeople[i].Agepeople[j].Age// 升序})slices.SortFuncGo 1.21slices.SortFunc(people,func(a,b Person)int{returna.Age-b.Age// 升序a-b降序b-a})cmp(a, b)返回负数 - a 排前面正数 - b 排前面0 - 相等。1.4 数组 vs 切片数组切片长度固定编译期确定动态运行时可变类型[3]int和[5]int是不同类型[]int统一类型传参值拷贝传切片头廉价[...]int{1,2,3}是数组初始化的语法糖编译器自动推断长度结果等同于[3]int{1,2,3}仍是固定长度数组。不能用于函数参数或类型声明。1.5 可变参数funcsum(nums...int)int{...}// nums 内部是 []intsum(1,2,3)// 直接传sum(nums...)// 展开切片传入1.6 基本数据类型分类类型布尔bool有符号整数int8int16int32int64int无符号整数uint8uint16uint32uint64uintuintptr浮点float32float64复数complex64complex128字符串string别名byte(uint8)rune(int32)1.7 rune 与 stringruneUnicode 码点int32一个整数编号stringUTF-8 编码后的字节序列varrrune中// 码点: 20013 (0x4E2D)varsstring中// UTF-8: E4 B8 AD3字节s:string(r)// 码点 → UTF-8 编码r,_:utf8.DecodeRuneInString(s)// UTF-8 → 码点字符串索引s[i]返回byte(uint8)range遍历返回rune(int32)。二、make vs newnew(T)make(T)返回值*T指针T值初始化零值不初始化内部结构完整初始化可直接使用适用范围任何类型仅 slice、map、channel类型make字面量var 声明var 后能否直接用切片可可可 nil切片append 可读写可Map可可可 nil map读取可写入 panicChannel可不可可 nil channel发送/接收永久阻塞close panic2.2 切片五种声明方式对比方式类型lencap nil能直接用make([]int, 0)[]int00false可[]int{}[]int00false可new([]int)*[]int00*s niltrue需解引用var a []int[]int00trueappend 可var a *[]int*[]int——a niltruepanic!new([]int)vsvar a *[]int// new([]int) → 指针非 nil指向一个 nil 切片b:new([]int)// b ! nil指针本身有值// *b nil指向的切片是 nil// *b append(*b, 1) ✅ 可以用// var a *[]int → 指针本身是 nil什么都没指向vara*[]int// a nil// *a → panic! 解引用 nil 指针// 必须先分配a new([]int) 或 a []int{} 才能用三、并发编程3.1 ChannelChannel 是 goroutine 之间传递数据和同步的管道。Go 哲学不要通过共享内存来通信而要通过通信来共享内存。无缓冲 vs 有缓冲无缓冲有缓冲创建make(chan int)make(chan int, N)发送阻塞没人接收就阻塞缓冲区满才阻塞接收阻塞没人发送就阻塞缓冲区空才阻塞本质同步一对一握手异步生产者-消费者关闭 Channel规则说明谁发送谁关闭只有发送方关闭不能关闭已关闭的通道panic不能向已关闭的通道发送panic不是必须关闭唯一必须关闭的场景是接收方用range遍历时。不关闭range会死锁。方向限定funcsend(chchan-int){}// 只写funcrecv(ch-chanint){}// 只读3.2 获取协程执行结果goroutine 的返回值会被丢弃只能通过 channel 获取结果。typeResultstruct{IDintValueintErrerror}ch:make(chanResult,3)fori:0;i3;i{gofunc(idint){val,err:doWork(id)ch-Result{ID:id,Value:val,Err:err}}(i)}fori:0;i3;i{r:-ch fmt.Printf(任务%d: 结果%d\n,r.ID,r.Value)}3.3 GMP 调度模型G - Goroutine 协程用户态轻量线程 M - Machine 操作系统线程内核态 P - Processor 逻辑处理器调度上下文P 是 GMP 的灵魂M 必须持有 P 才能执行 G。M 从哪拿 G优先级1. P 的本地队列LRQ ← 优先无锁 2. 全局队列GRQ ← 每 61 次取 1 个有锁但极少 3. 其他 P 偷work stealing ← 本地空了才偷 4. 网络轮询器netpoll ← 等 IO 的 G 5. 阻塞等待 ← 实在没有就休眠两种阻塞的区别系统调用阻塞Channel/IO 阻塞谁阻塞了M 阻塞线程卡在内核G 阻塞协程挂起P 要不要解绑必须M 卡住了不需要M 还活着M 的去向等系统调用返回 [系统调用返回后M 优先找回原 P原 P 忙就找其他空闲 P都没有就把 G 放全局队列、M 自己休眠。M 不会干等着G 也不会丢。]继续执行其他 GP 的去向找新 M 绑定hand off不动继续用当前 M全局队列的锁竞争全局队列确实有锁但访问频率极低P 优先从本地无锁队列取 G99%每 61 次调度才从全局队列取 1 个取时批量获取减少访问次数本地队列满溢出时才放全局队列抢占式调度G 执行超过 10ms 被标记为抢占Go 1.14 基于信号的抢占即使死循环也能被踢走3.4 Goroutine 轻量在哪里3.4.1 栈空间小操作系统线程固定栈 1~8MB创建即分配 Goroutine初始栈 2KB按需增长最多到 1GB1GB 内存能跑多少 线程1GB / 8MB ≈ 125 个 Goroutine1GB / 2KB ≈ 50 万个Goroutine 的栈是动态增长的初始 2KB不够用时自动扩容复制到更大的栈。3.4.2 创建/销毁成本低操作系统线程创建要陷入内核系统调用分配栈内存初始化内核数据结构 Goroutine纯用户态创建只分配一个 G 结构体 2KB 栈3.4.3 切换成本低线程切换Goroutine 切换陷入内核要不要切换页表要不要共享地址空间保存恢复寄存器全部16个少量3~5个缓存影响大TLB 刷新小耗时1~10μs100~200ns3.4.4 调度在用户态线程调度内核调度器决定无法控制何时切换 Goroutine调度Go runtime用户态决定GMP 模型调度线程阻塞 → 内核切换整个线程卡住Goroutine 阻塞channel→ 只挂起 GM 继续跑其他 G总结对比线程Goroutine栈大小1~8MB 固定2KB 起步动态增长创建成本系统调用重用户态轻切换成本1~10μs100~200ns调度者内核Go runtime用户态1GB 内存~125 个~50 万个一句话Goroutine 轻量在于初始栈仅 2KB线程 1~8MB、创建和切换都在用户态不陷入内核、切换只保存少量寄存器约 100ns vs 线程 1~10μs所以一台机器能轻松跑几十万个 Goroutine。3.5 GOMAXPROCS3.5.1 核心含义GOMAXPROCS P 的数量 最多同时运行的 M 数量 并行度// 默认值 CPU 逻辑核心数runtime.NumCPU()// 比如 8runtime.GOMAXPROCS(0)// 查看当前值返回 8// 手动设置runtime.GOMAXPROCS(4)// 限制只用 4 个线程3.5.2 对 GMP 的影响GOMAXPROCS 4 创建 4 个 PP0 P1 P2 P3 最多 4 个 M 同时并行执行 G 即使你有 10 万个 goroutine同时跑的也只有 4 个GOMAXPROCS1 时 P0—M0 → 所有 G 排队轮流跑并发不并行 GOMAXPROCS4 时 P0—M0 → G1 G3 G5 ... P1—M1 → G2 G4 G6 ... ← 4 个真正同时跑并行 P2—M2 → G7 G9 ... P3—M3 → G8 G10 ...3.5.3 注意M 的数量可以超过 GOMAXPROCSruntime.GOMAXPROCS(4)// 4 个 P// 但 M 可能多于 4 个// 原因系统调用阻塞时 M 卡住P 会找新 M// 所以 M 数量 活跃 M≤P数量 阻塞中 MP0—M0(G1 系统调用阻塞) ← M0 卡住 P0—M1(G2) ← P0 解绑后找新 M1 此时 M 数量 2但 P 仍然 13.5.4 可以设超过 CPU 核心数吗技术上可以但不推荐。4核 CPU设 GOMAXPROCS100 → Go 创建 100 个 P最多 100 个 M 同时抢 4 个核 → 4个真正在跑96个在等待 → 内核线程切换开销暴涨1~10μs/次 → TLB 缓存频繁失效 → CPU 缓存命中率下降 → 实际性能反而下降3.5.5 什么时候要改场景设置日常开发不用改默认 CPU 核心数CPU 密集型保持默认多了反而增加切换开销IO 密集型可以适当调大如 4 核设 6让更多 G 并行等 IO容器环境注意默认取宿主机核心数不是容器限制3.5.6 容器中的坑// 宿主机 32 核容器限制 2 核runtime.GOMAXPROCS(0)// 返回 32不是 2// Go 1.5~1.21 的坑默认创建 32 个 P但容器只有 2 核// → 线程竞争严重性能下降// 解决方案runtime.GOMAXPROCS(2)// 手动设为容器限制的核心数// Go 1.22 自动识别 cgroup 限制不再需要手动设一句话GOMAXPROCS P 的数量 最大并行度默认等于 CPU 核心数。它限制的是同时执行 Go 代码的线程数不限制 goroutine 数量也不限制因系统调用阻塞而产生的额外线程数。设超过 CPU 核心数只会增加线程竞争和切换开销降低性能。四、垃圾回收GC4.1 核心特点特点说明算法三色标记清除触发方式并发不 Stop The World大部分时间不支持无分代、无压缩4.2 三色标记法颜色含义白色未被访问GC 结束后回收灰色已访问但引用的对象还没全访问黑色已访问且引用的对象都已访问过程从根对象出发标灰 → 标黑 引用标灰 → 直到没有灰色 → 剩余白色 垃圾。4.3 golang V1.5插入写屏障机制并发标记时用户代码可能修改引用关系导致漏标。写屏障在每次修改引用时强制将被引用的对象标灰防止漏标。插入写屏障只对堆生效栈上没有写屏障栈操作太频繁加屏障性能损失大。GC 完整流程:1. 标记准备STW极短 → 开启插入写屏障根对象标灰 2. 并发标记和用户代码并行→ 扫描灰色对象将灰色对象标黑且将其引用的对象标灰插入写屏障保护发生引用修改即新增一个对象引用另外一个对象时将被引用对象标灰 3. 标记终止STW极短 → 重扫描栈处理剩余灰色关闭写屏障 4. 并发清除和用户代码并行→ 回收白色对象STWStop The World暂停所有用户代码只让 GC 干活。Go 只在开头和结尾各 STW 极短时间微秒级。为什么不对栈上的对象开启写屏障: 因为go在并发运行时大部分的操作都发生在栈上函数的调用会非常频繁。数十万goroutine的栈堆进行屏障保护自然会有性能问题。缺点Go 1.5 的插入写屏障要求堆上对象在标记结束前必须是灰色或黑色但栈上对象没有写屏障保护所以必须在 STW 期间重新扫描所有栈。这是 Go 1.5 STW 偏长的根本原因。1.5 的标记终止 STW 不是微秒级因为要重扫描所有 goroutine 的栈通常在 10~100ms 级别。V1.8以后达到微妙级别。4.4 golang V1.8混合写屏障机制插入写屏障删除写屏障[1] GC刚开始的时候会将栈上可达对象全部标记为黑色[2] GC期间任何在栈上新创建的对象均为黑色[3] 堆上指针被覆盖时旧值被删除的引用标灰 ← Yuasa 删除屏障[4] 堆上写入新指针时新值新引用的对象标灰 ← Dijkstra 插入屏障[1][2]步只有一个目的将栈上的可达对象全部标黑最后无需对栈进行STW[也就是V1.5的第三步的STW]就可以保证栈上的对象不会丢失。有人说一直是黑色的对象那么不就永远清除不掉了这里强调一下标记为黑色的是可达对象不可达的对象一直会是白色直到最后被回收。GC完整流程:1. 标记准备STW极短 → 开启混合写屏障堆的根对象标灰栈的所有对象标黑 2. 并发标记和用户代码并行→ 堆扫描灰色对象将灰色对象标黑且将其引用的对象标灰混合写屏障保护新创建和删除的对象标灰栈: 所有新创建的对象标黑 3. 标记终止STW, 极短 → 关闭混合写屏障无需重扫描栈相对与V1.5可达到微妙级别 4. 并发清除和用户代码并行→ 回收白色对象4.5 面试角度总结如果你准备面试核心要记住这个演进线索Go 1.5插入写屏障仅堆→ 栈没有写屏障 → 标记终止要 STW 重扫描所有栈 → STW 10~100msGo 1.8混合写屏障插入删除仅堆→ GC 开始时栈全部标黑 新对象标黑 → 不需要重扫描栈→ 标记终止 STW 降到微秒级Go 1.14基于信号的抢占式调度→ 消除了死循环导致 GC 无法触发的问题面试一句话答法Go 1.5 用 Dijkstra 插入写屏障只保护堆栈没屏障所以标记终止要 STW 重扫描栈停顿在 10~100msGo 1.8 改用混合写屏障插入删除GC 开始时把栈上可达对象全部标黑期间新对象也标黑这样就不需要重扫描栈了STW 降到微秒级。4.6 GC 触发条件条件说明堆内存增长到阈值默认 GOGC100堆翻倍时触发2分钟未触发强制触发一次手动调用runtime.GC()4.7 减少GC压力减少堆分配用值类型sync.Pool复用对象预分配切片make([]int, 0, N)strings.Builder代替字符串拼接五、init 函数在 main 函数之前包被导入时自动执行不能被调用不能有参数和返回值一个文件可有多个 init按定义顺序执行跨包依赖的包先初始化只执行一次即使包被多次导入执行顺序全局变量初始化 → init → main六、defer延迟执行函数返回前调用先进后出栈参数在 defer 时就确定最常用场景资源释放Close/Unlockdefer 可以修改命名返回值循环中 defer 会堆积改用匿名函数funcwork(){mu.Lock()defermu.Unlock()// 不怕中间 return 忘记解锁}七、recover只有在 defer 中调用 recover 才能捕获 panic其他地方调用返回 nil。deferfunc(){ifr:recover();r!nil{fmt.Println(捕获:,r)}}()八、装饰器Go 没有装饰器语法用函数包装实现functimer(fnfunc())func(){returnfunc(){start:time.Now()fn()fmt.Printf(耗时: %v\n,time.Since(start))}}work:timer(work)// 包装work()HTTP 中间件是最典型的装饰器模式。九、JSON 处理typeUserstruct{Namestringjson:nameAgeintjson:ageEmailstringjson:email,omitemptyPassstringjson:-}// 解析varuser User json.Unmarshal([]byte(jsonStr),user)// 必须传指针// 序列化data,_:json.Marshal(user)tag作用json:nameJSON 字段名为 namejson:-忽略该字段json:name,omitempty为零值时省略注意数字默认解析为float64。十、反射reflect10.1 TypeOf vs ValueOfreflect.TypeOf(x)→ reflect.Type → 类型信息名字、字段、tag、方法签名 reflect.ValueOf(x)→ reflect.Value → 值信息读值、改值、调方法10.2 操作结构体t:reflect.TypeOf(u)// Type 上v:reflect.ValueOf(u)// Value 上// 字段t.NumField()// 字段数量t.Field(i).Name// 字段名t.Field(i).Tag.Get(json)// tagv.Field(i)// 字段值v.FieldByName(Name)// 按名字取值// 方法t.NumMethod()// 方法数量t.Method(i).Name// 方法名v.MethodByName(X).Call(args)// 调用方法10.3 Map 相关// Type 上t.Key()// map 的 key 类型t.Elem()// map 的 value 类型 / slice 元素类型 / chan 元素类型// Value 上v.MapKeys()// 所有 keyv.MapIndex(key)// 根据 key 取 value10.4 修改值必须传指针v:reflect.ValueOf(x)vv.Elem()// 解引用v.SetInt(100)// 修改10.5 Kind 种类reflect.Int/reflect.String/reflect.Bool/reflect.Struct/reflect.Map/reflect.Slice/reflect.Ptr/reflect.Chan/reflect.Func十一、内存分配三级结构11.1 整体架构┌──────────────────────────────────────────────────┐ │ mheap │ │ 全局唯一从 OS 申请大块内存按类别管理 │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │mcentral │ │mcentral │ │mcentral │ ... │ │ │ (8B) │ │ (16B) │ │ (32B) │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ │ │mcache │ │mcache │ │mcache │ ... │ │ │(P0) │ │(P1) │ │(P2) │ │ │ └─────────┘ └─────────┘ └─────────┘ │ └──────────────────────────────────────────────────┘11.2 mcache每个 P 一个类比每个人的工具箱无锁拿工具。每个 P 绑定一个 mcache无锁分配速度最快大部分小对象从这里分配mcache 没货了才找 mcentralP0—mcache: [8B块][16B块][32B块][64B块]... P1—mcache: [8B块][16B块][32B块][64B块]...每种规格只有一个 mspan用满了就找 mcentral 换一个新的旧的还回去一换一。11.3 mcentral每个规格一个类比公共仓库某个规格的工具箱空了来进货。每种规格一个 mcentral8B、16B、32B… 共 67 种有锁但竞争不严重只有 mcache 空了才来mcentral 没货了才找 mheapmcentral(8B): [span1][span2][span3]... ← 8字节规格 mcentral(16B): [span1][span2][span3]... ← 16字节规格 mcentral(32B): [span1][span2][span3]... ← 32字节规格 ...67种规格11.4 mheap全局唯一类比总仓库从工厂OS进货。全局唯一大锁管理所有内存页8KB/页从 OS 申请内存mmap大对象32KB直接从 mheap 分配11.5 mspan 是什么mspan 是内存分配的基本单位是连续的内存页。一个 mspan N 个连续页每页 8KB mspan(32B规格, 8页): ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │ 32B │ 32B │ 32B │ 32B │ 32B │ 32B │ 32B │ 32B │ ... │ 空闲 │ 已用 │ 空闲 │ 已用 │ 已用 │ 空闲 │ 空闲 │ 已用 │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ 8页 × 8KB 64KB ÷ 32B 2048 个槽位11.6 内存规格Size ClassGo 把小对象分成 67 种规格class 0: 微小对象≤16B用 tiny 缓存 class 1: 8B class 2: 16B class 3: 32B class 4: 48B class 5: 64B ... class 67: 32768B (32KB)// 分配 24B → 找最接近的规格 → 32B// 分配 50B → 找最接近的规格 → 64B// 会有内部碎片但避免了外部碎片11.7 分配流程分配对象 │ ├─ 微小对象≤16B → mcache 的 tiny 缓存 │ ├─ 小对象16B~32KB → mcache → mcentral → mheap │ └─ 大对象32KB → 直接从 mheap 分配分配 24 字节对象 │ │ 1. 确定规格24B → 32B 规格 ↓ mcache(32B) 有空闲 ├─ 有 → 直接分配无锁 ✅99%走这里 │ │ 没有 ↓ mcentral(32B) 有空闲 mspan ├─ 有 → 拿一个 mspan 给 mcache再分配有锁但极少 │ │ 没有 ↓ mheap 有空闲页 ├─ 有 → 切成 mspan 给 mcentral → 给 mcache → 分配 │ │ 没有 ↓ 向 OS 申请内存mmap → 切成 mspan → mcentral → mcache → 分配11.8 回收流程对象不被引用 │ ├─ GC 标记为垃圾 ↓ mcache 中的空闲 mspan │ ├─ 归还给 mcentral ↓ mcentral 中大量空闲的 mspan │ ├─ 归还给 mheap ↓ mheap 空闲页 │ ├─ 大量空闲时归还给 OSmadvise11.9 三级对比mcachemcentralmheap数量每个 P 一个每种规格一个全局唯一锁无锁有锁每规格独立全局大锁速度最快较快最慢分配对象小对象≤32KB给 mcache 补货大对象 给 mcentral 补货类比个人工具箱规格仓库总仓库从OS进货11.10 为什么这样设计核心目标减少锁竞争 无 mcache所有分配都找 mcentral: P0 → mcentral P1 → mcentral ← 所有 P 抢同一把锁 P2 → mcentral 有 mcache: P0 → mcache无锁✅ P1 → mcache无锁✅ ← 各取各的 P2 → mcache无锁✅ 只有 mcache 空了才找 mcentral极少和 GMP 的设计思路一样本地优先无锁快路径 全局慢路径兜底。
返回列表