2025-11-270

相关信息

golang基础学习

为什么选择go

  • 优势:
    • GC 自动管理内存
      • 不用开发人员手动管理、开发效率更高
    • goroutine 和 channel 的优势以及go的并发模型更加简单
      • 让并发编程相对简单,运行时调度成本低
      • 非常适合微服务、RPC 和网络服务
    • 占用内存低
    • 编译快、运行启动快
    • 标准库完善,云原生生态也非常成熟
  • 缺点
    • 错误处理比较繁琐
    • gc 有一点的开销

context的使用

  • 职责: 控制请求链路的生命周期
    • 取消信号
    • 超时时间/deadline
    • 少量请求级元数据:request_id、trace_id、日志字段等
  • 不放业务数据的原因
    • 类型不安全
      • context.Value的类型是any,取值需要类型断言
    • 依赖关系不清晰
      • 函数需要什么业务参数,应该体现在函数签名里,放进context后,调用双方的依赖会变为“隐式”
    • 滥用会让context变成全局map
      • context变成隐形参数包,排查问题困难
    • 生命周期不匹配
  • 总结

context 的核心用途是控制请求链路的生命周期,比如取消、超时和传递少量请求级元数据。业务数据应该通过明确的函数参数传递,而不是放到 context.Value 里。否则会带来类型不安全、依赖隐式、可读性差和滥用成全局 Map 的问题。Context 里可以放 trace id、request id 这类跨函数、跨服务的元信息,但不应该放订单对象、用户实体这类核心业务数据

context的使用场景

  • context可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。
    • 最重要的是它是并发安全的。
  • 可以存储键值对,供上下文(协程间)读取【建议不要使用】
  • 优雅的主动取消协程(Cancel)。主动取消子协程运行,用不到子协程了,回收资源。比如一个http请求,客户端突然断开了,就直接cancel,停止后续的操作;
  • 超时退出协程(Timeout),比如如果三秒之内没有执行结束,直接退出该协程;
  • 截止时间退出协程(Deadline),如果一个业务,2点到4点为业务活动期,4点截止结束任务(协程)

Go 程序启动顺序

js
1. 编译期(build) 2. 生成可执行文件 3. 运行时启动(runtime start) 4. 初始化 package 级别变量 5. 执行 init() 6. 执行 main.main()
  • 备注: 局部变量是在函数执行调用时,才会被触发声明

编译时做了那些事情

js
1. 语法检查 2. 类型检查 3. 依赖分析 4. 变量初始化代码生成(决定怎么初始化、按什么顺序初始化) 5. init 调用链生成 6. main 入口标记

gc 垃圾回收

  • Go 的垃圾回收器采用的是并发三色标记清除 + 混合写屏障
    • GC 开始时会短暂 STW,开启写屏障,
      • 目的:获得一致的 Root 快照,保证标记起点正确
    • 然后进入并发标记阶段,
      • 从 GC Roots 出发,利用三色标记遍历所有可达对象。
      • 标记过程中,由于用户 Goroutine 仍在运行,可能修改对象引用关系,
      • 因此 Go 使用混合写屏障维护三色不变性,防止存活对象漏标。
    • 标记结束后再次进行一次很短的 STW,
      • 目的: 保证标记完整,没有漏标对象,才能安全开始回收
    • 随后并发清扫所有未标记的对象并释放内存
  • 混合写屏障解决了什么问题
    • 在并发标记期间维护三色不变性,防止黑色对象直接引用未标记的白色对象,从而避免存活对象被错误回收
  • 混合写屏障具体做了什么
      1. 被删除的旧引用对象,如果还没标记,要标灰
      1. 新写入的引用对象,如果还没标记,也要标灰

内存逃逸

  • 什么是内存逃逸
    • 编译器的一种内存分配策略,对象从栈分配变成了堆分配。
  • 逃逸一定就不好么
    • 逃逸意味着
      • 多一次堆分配;
      • 增加 GC 压力
    • 如果对象生命周期确实超过了函数调用
      • 逃逸就是正确且必要的
  • 场景
    • 给一个引用类对象中的引用类成员进行赋值
      • 可以理解为访问一个引用对象实际上底层就是通过一个指针来间接的访问了
      • 但如果再访问里面的引用成员就会有第二次间接访问
      • 这样操作这部分对象的话,极大可能会出现逃逸的现象
  • case
    • 返回局部变量的地址
    • 闭包捕获局部变量
    • 引用类型的返回
    • 被接口(interface)引用
    • goroutine 使用局部变量
    • 返回包含底层数据的 slice、map 等,使底层对象需要继续存活
    • 对象过大,不适合放在栈上

内存泄漏

  • 核心原因
    • 长期持有对象引用,导致gc无法回收
  • case:
    • 从一个大的slice中截取一小段
      • 只要这部分小的slice一直在使用,那么这个大的slice就一直无法被gc
      • 通过copy(newSlice, large[0:1])
    • channel阻塞卡死
    • channel没有关闭
    • 全局变量、map、slice一直持有引用,导致对象一直可达
    • map的无限增大
    • slice 持有大数组
    • goroutine 因阻塞在 channel、锁或网络 I/O 上无法退出,造成泄漏
      • goroutine 中的消费一直未写入ch导致额永久阻塞
      • goroutine 中写入的ch,一直无法消费导致的阻塞
    • context 未 cancel
    • 不会因为循环引用导致泄漏
      • 标记-清除(Mark & Sweep)GC
    • time.Ticker 或 time.Timer 使用后没有及时 Stop
    • 缓存(如 map、sync.Map)没有淘汰策略,数据持续增长

如何避免channel被重复关闭

  • 重复关闭会发生什么
    • panic
  • 怎么避免
    • 通过sync.once
    • 遵循“由发送者关闭”原则

grpc是什么

  • 概念
    • gRPC 是一个基于 HTTP/2 和 Protobuf 协议的高性能 RPC 框架,
    • 支持多语言和流式通信,
    • 主要用于微服务之间的高性能调用。
    • 相比 REST,它序列化效率更高、连接复用能力更强,
    • 但可读性较差,
    • 更适合内部服务通信。
  • 优势
    • RPC 底层使用 HTTP/2 ,相比 HTTP/1.1 有几个优势:
      • 多路复用(Multiplexing):
        • 一个 TCP 连接可以同时传输多个请求,
        • 不需要像 HTTP/1.1 那样为并发请求建立多个连接或排队等待。
      • 二进制分帧:
        • 传输效率更高,协议解析开销更小。
      • Header 压缩(HPACK):
        • 减少重复请求头带来的网络开销。
    • 使用 Protocol Buffers,相比 JSON,序列化效率高
      • 数据更小
      • 编解码速度更快
      • 类型明确
    • 通过 .proto 定义接口
      • 可以自动生成多语言客户端和服务端代码
      • 具有强类型约束,开发效率高
    • 原生支持 Unary、服务端流、客户端流和双向流四种 RPC 模式
      • 非常适合微服务和实时通信场景
    • 官方生态完善

匿名函数

  • 减少代码污染,避免定义一次性函数
  • 实现闭包
    • 闭包可以捕获外部变量
  • 传值问题
    • 不传值的情况下,匿名函数捕获的是变量,而不是值
    • 一般建议传值

Go 支持什么形式的类型转换?

  • 不支持隐式的类型转换,必须进显示转换
    • 基本类型 使用 T(v) 的方式进行转换
      • case:
        golang
        var f float64 = 3.1 i := int(f) // 3
    • 字符串与 []byte / []rune 转换:
      • 用于处理字节数据和 Unicode 字符。
        js
        s := "你好" r := []rune(s) fmt.Println(len(s)) // 6 fmt.Println(len(r)) // 2
    • 类型断言(Type Assertion):用于将接口类型转换为具体类型,
      • 常见形式为 v := i.(T) 或 v, ok := i.(T)。
    • 类型 switch:
      • 基于类型断言,对接口的动态类型进行分支处理。
    • 接口之间的转换/赋值:
      • 满足方法集的接口可以相互赋值
        golang
        type Reader interface { Read([]byte) } type ReadWriter interface { Read([]byte) Write([]byte) } var rw ReadWriter var r Reader = rw
    • 自定义类型与底层类型转换:
      • 底层类型相同也需要显式转换。
    • unsafe.Pointer 转换:底层指针转换,绕过 Go 的类型安全机制。

服务 goroutine 数量一直在涨,应该如何排查

  • 我会先通过 runtime.NumGoroutine 和监控确认 goroutine 是否持续上涨且不回落,
  • 然后用 pprof goroutine 抓堆栈,查看大量 goroutine 卡在哪类调用上。
  • 常见原因包括
    • channel 没人收发、
    • context 没有取消、
    • ticker 没有 Stop、
    • 外部 IO 没有超时、
    • WaitGroup 没有 Done、
    • 以及无限制创建 goroutine。
  • 定位后会针对性修复,
    • 比如所有 goroutine 都接收 ctx.Done(),
    • channel 明确关闭时机,
    • 外部调用设置超时,
    • ticker defer Stop,
    • 并用 worker pool 或 semaphore 控制并发数。

如何让goroutine自动退出

  • golang是无法强杀goroutine的
  • 通过context 的cancel 或timeout实现(推荐)
golang
func worker(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("worker exit") return default: fmt.Println("working...") time.Sleep(time.Second) } } } func main() { ctx, cancel := context.WithCancel(context.Background()) go worker(ctx) time.Sleep(3 * time.Second) cancel() // 通知退出 time.Sleep(time.Second) }
  • 通过通道,通知协程退出

Go 语言中 cap 函数可以作用于哪些内容?

类型是否支持 cap返回值含义
数组(Array)数组长度
数组指针(Pointer to Array)指向数组的长度
切片(Slice)切片容量
通道(Channel)通道缓冲区容量

Printf(),Sprintf(),FprintF() 都是格式化输出,有什么区别

  • Print() 标准输出,但是无格式,打印多个参数时,参数输出之间自动增加一个空格
  • Printf()是标准输出,一般用于打印。
  • Sprintf()把格式化字符串输出到字符串,并返回
  • FprintF()把格式化字符串输出到实现了io.witer方法的类型,比如文件

golang中new和make的区别

  • 共同点:都会分配内存空间(堆上)
  • 不同点:
    • 作用变量不同,
      • new 可以为任意类型分配内存
      • make 只能给切片、map、chan分配内存
    • 返回类型不同
      • new返回的是指向变量的指针
      • make返回的是上边三种变量类型本身

一句话总结:new是初始化为对应类型的零值;make初始化零值的同时,也初始化分配结构,比如在设置长度、容量的时候

slice 扩容机制

  • Go 1.18 之后(新算法):不再以 1024 为界限,而是引入了更平滑的过渡:
    • 如果期望容量大于当前容量的两倍,直接使用期望容量。
    • 如果当前容量 < 256,容量翻倍(2x)。
    • 如果当前容量 ≥ 256,采用公式:newcap=oldcap+(oldcap+3×256)/4newcap = oldcap + (oldcap + 3 \times 256) / 4
  • 注:随着 oldcap 变大,扩容倍数会从 2x 逐渐降到 1.25x,曲线更平滑,避免了从 2x 突降到 1.25x 的震荡。

数组定义问题

数组是可以通过下标定义的

js
array := [...]int{1,2,3,9:34} 表示array[9]==34len(array)就是10

数据和切片的区别

  • 数组和slice 虽然都是值类型
    • 数组赋值会拷贝数据
    • slice具有引用语义,赋值会共享底层数组。
  • 数组长度是类型的一部分
    • 比如 [3]int 和 [4]int 是不同类型
    • 而 slice 长度是动态的,可以通过 append 扩容。
  • slice 本质是一个结构体,包含指向底层数组的指针、长度和容量,
  • 数组本质是一段连续内存。
  • 在函数传参时
    • 数组会发生完整拷贝
    • slice 只会拷贝结构体,但底层数据是共享的。

golang中所有传参都是值传递

  • 是Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝
  • 拷贝的内容为非引用类型(int、string、struct)时,在函数中无法修改原内容数据
  • 拷贝的内容为引用类型(指针、map、slice、chan)时,会修改原内容数据

rune类型

  • golang中string底层是通过byte数组实现的。
    • 中文字符在unicode下占2个字节,在utf-8编码下占3个字节,
    • golang默认编码正好是utf-8。
  • 所以len计算的时候,一个中文字符占用3个字节。
  • 而转换为rune计算,就刚好能得到字符的实际个数,比如我们想取出‘界’,rune的方式就很友好
js
import ( "fmt" "unicode/utf8" ) func main() { var str = "hello 世界" //golang中string底层是通过byte数组实现的 // 直接求len 实际是在按字节长度计算 所以一个汉字占3个字节算了3个长度 fmt.Println("len(str):", len(str)) //以下两种都可以得到str的字符串长度 //golang中的unicode/utf8包提供了用utf-8获取长度的方法 fmt.Println("RuneCountInString:", utf8.RuneCountInString(str)) //通过rune类型处理unicode字符 fmt.Println("rune:", len([]rune(str))) } // 结果为 12 8 8 fmt.Println(string([]rune(str)[7:])) //就能取出‘界’

golang 的map问题

  • 底层结构
    • 哈希表 + bucket
    • 一个map大概包含
    golang
    type hmap struct { count int B uint8 buckets unsafe.Pointer oldbuckets unsafe.Pointer ... }
  • map为什么会并发panic
    • 底层是哈希表,由多个bucket组成, 写操作对于底层结构的修改不是原子性的
    • 写操作对于底层结构的修改不是原子性的
      • 不仅会修改元素
      • 还可能更新bucket、溢出桶、元素数量
      • 甚至触发渐进式扩容和bucket 迁移
    • 如果多个 goroutine 同时读写或写写 map,可能导致内部状态不一致
    • Go Runtime 在 map 写入时会设置 hashWriting 标志位,当检测到并发读写或并发写时,会主动触发panic
  • map+RWMutex 是通用方案
    • 结构简单、类型安全,适合大多数业务场景,特别是读写比例比较均衡或者写操作较多的场景。
  • sync.Map 内部采用 read map 和 dirty map 的读写分离设计
    • 读操作大部分情况下通过 atomic 无锁完成,因此在读多写少、key 稳定、缓存类场景下性能更好。
      • 读操作是先从read map获取数据,获取不到才去dirty map读取
    • 但 sync.Map 写路径更复杂,频繁写入或删除时性能不一定优于 map + RWMutex

map扩容的时机

  • 等量扩容
    • Overflow Bucket 太多
  • 翻倍扩容
    • 负载因子(Load Factor)(元素个数/bucket个数)过高

map的渐进式扩容

  • 什么是渐进式扩容
    • 不是一次性将数据搬到新的bucket,而是后续每次访问map时,顺带搬迁一部分数据
  • 目的
    • 避免一次性扩容导致程序卡顿
  • go怎么做?
    • 创建新的buckets, 将原本的buckets设置为oldbuckets
    • 同时保留两份bucket
  • 扩容期间如何查找数据?
    • 如果已经搬迁,就去新的bucket查
    • 如果没有搬迁,就去旧的bucket查

nil

nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”

但是如果不特别指定的话,Go 语言不能识别类型

常量不能取值

  • 因为常量时编译器就替换为值,是没有地址的,
  • 所以不可以通过&常量名的方式获取地址的,只能print值

defer 的使用场景

场景描述
文件操作defer file.Close() 避免忘记关闭文件
锁操作defer mu.Unlock() 确保释放锁
数据库事务defer tx.Rollback()Commit()
捕获异常搭配 recover() 使用来捕获 panic

defer 的执行顺序

  • 先进后出
golang
func test() { defer fmt.Println("A") defer fmt.Println("B") defer fmt.Println("C") }

执行结果是:

golang
C B A

defer 的常见陷阱

go
for i := 0; i < 3; i++ { defer fmt.Println(i) } // 输出:2 2 2(不是 2 1 0)
  • 原因:
    • defer 延迟执行,i 的值在循环结束时为 2,所有 defer 引用的是同一个 i
  • 正确的写法
go
for i := 0; i < 3; i++ { v := i defer fmt.Println(v) } // 输出:2 1 0
  • 独立的recover 机制实现

重要:每个goroutine都需要独立的recover机制,否则panic会导致整个程序崩溃。

go
func safeGoRoutine() { defer func() { if r := recover(); r != nil { fmt.Println("Goroutine recovered:", r) } }() // goroutine的业务逻辑 panic("goroutine panic") } func main() { go safeGoRoutine() time.Sleep(time.Second) }

gin 框架的中间件是怎么实现的

  • Gin 的中间件是基于责任链的洋葱模型。
    • 每个中间件都是 "gin.HandlerFunc"
    • 通过 "c.Next()" 把控制权交给下一个 Handler,
    • "c.Abort()" 则直接终止链条。
  • 内部用 "handlers" 切片 + "index" 下标驱动,没有递归开销,性能很好。
  • 中间件按注册顺序形成"先进后出"的执行流,
  • 所以外层中间件适合做日志、耗时统计这类前后都需要处理的事情。
  • 注册方式有全局、路由组、单路由三种
    • 全局的一定最先执行。常见用途是鉴权、日志、跨域、异常恢复这些横切逻辑。

gc触发机制

  • 主动触发
    • runtime.GC()
  • 被动触发
    • 堆内存达到上次gc后的堆内存的2倍
    • 距离上次gc超过2分钟

goroutine如何动态的保活

  • 子Goroutine
    • 可以通过给一个被监控的Goroutine添加一个defer ,然后recover() 捕获到当前Goroutine的异常状态,最后给主Goroutine发送一个死亡信号,通过Channel。
  • 主Goroutine
    • 在主Goroutine上,从这个Channel读取内容,当读到内容时,就重启这个子Goroutine,当然主Goroutine需要记录子Goroutine的ID,这样可以针对性的启动了
  • 代码实现
js
type WorkerManager struct { //用来监控Worker是否已经死亡的缓冲Channel workerChan chan *worker // 一共要监控的worker数量 nWorkers int } //创建一个WorkerManager对象 func NewWorkerManager(nworkers int) *WorkerManager { return &WorkerManager{ nWorkers:nworkers, workerChan: make(chan *worker, nworkers), } } //启动worker池,并为每个Worker分配一个ID,让每个Worker进行工作 func (wm *WorkerManager)StartWorkerPool() { //开启一定数量的Worker for i := 0; i < wm.nWorkers; i++ { i := i wk := &worker{id: i} go wk.work(wm.workerChan) } //启动保活监控 wm.KeepLiveWorkers() } //保活监控workers func (wm *WorkerManager) KeepLiveWorkers() { //如果有worker已经死亡 workChan会得到具体死亡的worker然后 打出异常,然后重启 for wk := range wm.workerChan { // log the error fmt.Printf("Worker %d stopped with err: [%v] \n", wk.id, wk.err) // reset err wk.err = nil // 当前这个wk已经死亡了,需要重新启动他的业务 go wk.work(wm.workerChan) } }

装饰器模式

  • 核心思路
    • 在不修改原对象的情况下,给它动态增加功能
  • 用到装饰器模式的库
    • grpc
    • net/http

字符串的小问题

go
①可以用==比较 ②不可以通过下标的方式改变某个字符,字符串是只读的 ③不能和nil比较

空结构体

  • 不包含任何字段的结构体叫做空结构体 struct{}
  • 定义方式:
js
var et struct{} et := struct{}{} type ets struct {} / et := ets{} / var et ets
  • 特性:
    • 所有的空结构体的地址都是同一地址,都是zerobase的地址,且大小为0
  • 使用场景:
    • 用于保存不重复的元素的集合,Go的map的key是不允许重复的,用空结构体作为value,不占用额外空间。
    • 用于channel中信号传输,当我们不在乎传输的信号的内容的时候,只是说只要用信号过来,通知到了就行的时候,用空结构体作为channel的类型
    • 作为方法的接收者,然后该空结构体内嵌到其他结构体,实现继承

虚拟内容的作用

  • 物理内存无法被最大化利用。
  • 程序逻辑内存空间使用独立。
  • 内存不够,继续虚拟磁盘空间

本文作者:曹子昂

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!