GO 八股整理(自用)

slice和array的区别

维度array(数组)slice(切片)
长度特性长度固定(声明时指定,如[5]int长度可变(动态扩容)
类型标识长度是类型的一部分([3]int[5]int是不同类型)长度不是类型的一部分([]int统一类型)
内存结构直接存储元素(值类型)存储指针、长度、容量(引用类型,指向底层数组)
传递方式传值(复制整个数组)传引用(复制切片头,共享底层数组)

slice 的 len、cap、共享、扩容

  • len:切片当前元素个数;cap:底层数组的容量。
  • 共享:多个切片可指向同一底层数组,修改其中一个会影响其他切片。
  • 扩容:当appendlen>cap时,分配新数组并复制数据:
    • 原容量≤1024:新容量=原容量×2;
    • 原容量>1024:新容量=原容量×1.25;
    • 若追加元素过多,直接按“原长度+新增长度”扩容。

map、slice未初始化的操作结果及panic处理

  • 未初始化的slice:默认是nil,可执行读操作(返回零值)、len/cap(返回0),但写操作会panic
  • 未初始化的map:默认是nil,可执行读操作(返回零值)、len(返回0),但写操作会panic

panic的处理方式

  • 通过defer + recover()捕获panic,清理资源后恢复程序运行;
  • 避免panic的核心是:操作前初始化(slice = make([]int, 0)map = make(map[string]int))。

recover的使用及defer的优势

  • recover的使用
    必须在defer函数中调用,用于捕获当前Goroutine的panic,返回panic的错误信息(无panic时返回nil)。
    示例:

    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("捕获panic:%v", err)
        }
    }()
    
  • defer的优势
    相比“函数最后手动执行操作”,defer能保证操作无论函数正常返回还是panic,都会执行(如资源释放、锁解锁),避免资源泄漏。

map的有序性、并发安全

  • map是无序的
    Go的map底层是哈希表,遍历是按哈希桶的顺序遍历,与插入顺序无关;即使Go 1.21+对遍历做了随机化优化,仍不保证有序。

  • map不是并发安全的
    并发读写会直接panic。保证并发安全的方式

    1. map + sync.RWMutex(读写锁);
    2. sync.Map(标准库提供的并发安全map,适用于读多写少)。

控制GMP中M的数量

GMP是Go的调度模型(G:Goroutine,M:操作系统线程,P:处理器),可通过环境变量GOMAXPROCS控制P的数量(默认等于CPU核心数),但无法直接控制M的数量——M由Go运行时根据调度需求动态创建/销毁(默认无上限,极端场景可通过debug.SetMaxThreads限制)。

控制Goroutine的生命周期

  • channel的作用
    通过关闭channel或向channel发送信号,让Goroutine感知并退出(如done <- struct{}{})。

  • context的作用
    传递上下文(如超时、取消信号),控制Goroutine的生命周期,避免Goroutine泄漏(常用context.WithCancel/WithTimeout)。

select的使用

select用于同时监听多个channel的操作(读/写),语法类似switch,执行任意一个就绪的case;若所有case都未就绪,则阻塞(或执行default)。
示例:

select {
case msg := <-ch1:
    fmt.Println("收到ch1消息:", msg)
case ch2 <- 10:
    fmt.Println("向ch2发送消息")
default:
    fmt.Println("无channel就绪")
}

new和make的区别

函数适用类型返回值类型作用
new任意类型指针(*T分配内存,初始化零值
make仅slice、map、channel类型本身(T分配内存并初始化(如slice的底层数组)

context机制介绍

context是Go用于传递请求上下文的标准库(context.Context),核心作用是在Goroutine之间传递取消信号、超时信号、元数据,控制Goroutine的生命周期,避免资源泄漏。

核心特性:
  • 可派生:通过context.Background()(根上下文)派生子上下文(如WithCancel/WithTimeout/WithValue);
  • 链式传递:子Goroutine通过参数接收context,感知上游的取消/超时事件;
  • 不可修改Context接口的方法都是只读的,保证上下文的一致性。
常用方法:
  1. context.WithCancel(parent):派生可手动取消的上下文;
  2. context.WithTimeout(parent, duration):派生超时自动取消的上下文;
  3. context.WithValue(parent, key, val):传递元数据(仅用于请求范围的轻量数据)。
示例(控制Goroutine退出):
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done(): // 感知取消/超时
            fmt.Println("worker退出:", ctx.Err())
            return
        default:
            fmt.Println("worker运行中...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel() // 确保资源释放
    go worker(ctx)
    time.Sleep(5 * time.Second)
}

GC回收原理(Go 1.5+ 并发标记清除)

Go的GC是并发标记清除(CMS) 算法,核心目标是低延迟,分为5个阶段:

阶段1:STW - 初始标记(Initial Mark)
  • 行为:暂停所有Goroutine(STW,Stop The World),标记根对象(如全局变量、Goroutine栈上的对象);
  • 特点:耗时极短(通常微秒级)。
阶段2:并发标记(Concurrent Mark)
  • 行为:恢复Goroutine运行,后台GC线程并发遍历堆对象,标记所有可达对象;
  • 辅助:Goroutine分配内存时会触发“写屏障”,记录对象引用的变化,保证标记的准确性。
阶段3:STW - 终止标记(Mark Termination)
  • 行为:再次暂停所有Goroutine,处理并发标记期间的竞争条件(如标记遗漏的对象);
  • 特点:耗时短,通常微秒级。
阶段4:并发清除(Concurrent Sweep)
  • 行为:恢复Goroutine运行,后台GC线程并发清理未标记的对象(释放内存);
  • 特点:不影响业务Goroutine,清理后的内存会被标记为空闲,供后续分配。
阶段5:并发清理(Concurrent Scavenge)
  • 行为:GC线程将空闲内存归还给操作系统(可选,根据内存使用情况触发);
  • 特点:避免进程占用过多物理内存。
核心优化:
  • 写屏障:并发标记时记录对象引用变化,保证标记准确性;
  • 分代标记(Go 1.19+):优先标记新分配的对象(存活时间短),提升标记效率;
  • 低延迟优先:STW阶段耗时控制在毫秒级甚至微秒级,适合高并发服务。

Go 写屏障(Write Barrier)

写屏障是 Go GC 在并发标记阶段保证标记准确性的核心机制——当 Goroutine 并发修改对象引用时,写屏障会记录引用的变化,避免 GC 遗漏可达对象(解决“并发标记时对象引用被修改”的竞争问题)。

核心作用:

在并发标记阶段,Goroutine 仍在运行并修改对象的引用关系,写屏障可以拦截所有“对象引用更新”操作,将新引用的对象标记为“可达”,确保 GC 不会错误地回收仍在使用的对象。

Go 写屏障的类型(演进):

Go 经历了两次写屏障的优化,目前默认使用 混合写屏障(Go 1.8+)。

(1)Dijkstra 写屏障(Go 1.5-1.7)
  • 规则:当修改对象 a 的字段,将其引用从 b 改为 c 时,标记新对象 c 为可达
  • 问题:需要额外标记栈上的对象,导致 STW 时间较长。
(2)混合写屏障(Go 1.8+,当前默认)

结合了 Dijkstra 写屏障和 Yuasa 写屏障的优点,无需 STW 扫描栈,进一步降低延迟。

  • 规则
    1. 当修改对象 a 的字段时,标记旧对象 b 为可达
    2. 当 Goroutine 创建新对象时,直接标记新对象为可达
    3. 当 Goroutine 从栈上读取对象并赋值给堆对象时,标记该栈对象为可达
  • 优势:并发标记阶段无需暂停 Goroutine 扫描栈,STW 时间大幅缩短。
写屏障的触发时机:

仅在 GC 并发标记阶段 启用写屏障;GC 其他阶段(如初始标记、清除)不会触发写屏障,避免不必要的性能开销。

总结:

写屏障是 Go GC 实现“并发标记”的关键技术,通过拦截对象引用的修改操作,保证在 Goroutine 并发运行时,GC 仍能准确标记所有可达对象,最终实现低延迟的并发垃圾回收。

sync.map和map加锁

sync.Map 和「原生 map + 锁(sync.Mutex/sync.RWMutex)」都能实现线程安全的并发读写,但二者的设计思路、性能特性、适用场景差异极大,核心结论先明确:

实现方式是否线程安全核心原理
sync.Map✅ 是双表(read/dirty)分离 + 原子操作 + 轻量锁,读无锁、写加锁,针对“读多写少”优化
原生 map + sync.Mutex✅ 是全局互斥锁,所有读写操作都加锁,串行执行,无读写分离
原生 map + sync.RWMutex✅ 是读写锁,读操作加共享锁(多goroutine可同时读)、写操作加排他锁(独占)

一、核心差异(关键选型依据)

1. 性能特性
  • sync.Map
    读操作(Load)无锁(直接原子读 read 表),写操作(Store/Delete)仅在更新 dirty 表时加锁,读多写少场景性能极高
    但写频繁场景下,dirty 表向 read 表迁移(misses 阈值触发)会带来额外拷贝开销,性能反而下降。

  • map + Mutex
    所有操作串行(读/写都要等锁),读写都频繁的场景性能最差,但逻辑最简单、无额外开销。

  • map + RWMutex
    读操作可并发(共享锁),写操作独占,读多写少场景性能接近 sync.Map,写频繁场景比 sync.Map 更稳定(无数据拷贝开销)。

2. 适用场景
实现方式最佳适用场景避坑场景
sync.Map读极多、写极少(如配置缓存、静态数据查询)写频繁(如高频更新的业务数据)
map + Mutex读写都不频繁(如低频配置修改)、逻辑极简场景高并发读/写
map + RWMutex读多写少(通用场景)、写操作不频繁但需稳定写操作极频繁(排他锁会阻塞所有读)
3. 功能特性
  • sync.Map 额外提供 LoadOrStore(加载或写入)、Range(遍历)等专用方法,且无需初始化(直接用);
  • 原生 map + 锁 需手动初始化 map,遍历需先加锁(避免遍历时写操作导致 panic),功能更灵活(可自定义锁粒度)。

二、代码示例(对比实现)

1. sync.Map 实现线程安全
package main

import (
    "sync"
    "fmt"
)

func main() {
    var sm sync.Map
    var wg sync.WaitGroup

    // 100个goroutine写
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            sm.Store(n, n*10) // 写操作
        }(i)
    }

    // 1000个goroutine读
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            if v, ok := sm.Load(n%100); ok { // 读操作
                fmt.Printf("key:%d, val:%d\n", n%100, v.(int))
            }
        }(i)
    }

    wg.Wait()
}
2. map + RWMutex 实现线程安全
package main

import (
    "sync"
    "fmt"
)

func main() {
    m := make(map[int]int)
    var rwmu sync.RWMutex
    var wg sync.WaitGroup

    // 100个goroutine写(加排他锁)
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            rwmu.Lock()         // 写加排他锁
            m[n] = n*10
            rwmu.Unlock()
        }(i)
    }

    // 1000个goroutine读(加共享锁)
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            rwmu.RLock()         // 读加共享锁
            if v, ok := m[n%100]; ok {
                fmt.Printf("key:%d, val:%d\n", n%100, v)
            }
            rwmu.RUnlock()
        }(i)
    }

    wg.Wait()
}

三、总结

  1. 二者都能实现线程安全,但选型核心看读写比例
    • 读多写极少 → 优先 sync.Map(无锁读,性能最优);
    • 读多写少(通用场景)→ 优先 map + RWMutex(稳定、灵活);
    • 读写都少 → map + Mutex(逻辑简单);
  2. sync.Map 是 Go 官方为“读多写少”场景定制的优化方案,而非替代所有加锁 map
  3. 写频繁场景下,map + RWMutexsync.Map 更优(无 dirty 表拷贝开销)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值