Skip to content

  • java 和 go 的区别
  • 线程、进程和 goroutine 的区别
  • 你用过泛型么,接口和泛型的区别
  • 内存泄露和内存逃逸
  • map 底层
  • gin 的工作原理
  • GMP 并发模型
  • sync.map 底层是如何实现的?

go 中 make 和 new 的区别

特性newmake记住的口诀
作用对象任意类型只用于 slice / map / channelnew 啥都行,make 只限三种
返回值*T(指针)T(值本身)new 给地址,make 给东西
是否初始化内容只分配零值内存分配内存 + 初始化内部结构new 只给空壳,make 给成品
常见使用场景结构体、基本类型指针创建可用的 slice/map/channel——
零值表现&T{}(所有字段零值)长度/容量为 0 的 slice、空的 map、空 channel——
  • new:只负责分配内存 + 返回指针,内容是零值
  • make:分配内存 + 初始化内部数据结构,返回可直接使用的值(只给 slice/map/channel 用)

java 和 go 的区别

Java 用重量级线程做并发,切换成本高、并发量有限,数据共享主要靠锁;Go 用用户态协程,切换轻、能轻松扛住数万级并发,并用 channel 做数据传递减少竞争。Go 的 goroutine 是用户态协程,切换成本极低所以能轻松跑上万并发;Java 线程是内核线程,栈大且切换昂贵,线程数一多就容易因为内存占用过高而 OOM

Java 依赖 JVM 运行、部署偏重,而 Go 是直接编译成二进制上服务器即跑

语言上 Java 功能齐全但更啰嗦,Go 刻意保持简洁

Java 多用于大数据、电商等企业级业务,而 Go 更适合中间件、高性能服务和云原生场景

线程、进程和 goroutine 的区别

进程和线程的切换都要让操作系统介入,保存大量状态,因此一次切换成本高、频率上来后性能急剧下降;而 goroutine 属于用户态协程,由 Go runtime 自己调度,切换时只改少量状态,不触发系统调用,所以切换开销更低,能在几十个内核线程上承载成千上万个 goroutine,从而在高并发场景下表现出远高于线程的效率

你用过泛型么,接口和泛型的区别

接口解决的是行为多态,泛型解决的是类型复用。接口像我不管你是啥,只要你会这些动作我就能用你;泛型更像你得是这一类东西,我才能让你进模板里编译

内存泄露和内存逃逸

内存泄露就是:本来应该被 GC 回收的对象,因为你的代码还在某处引用它,导致永远不会释放。

内存逃逸是编译器决定某个变量不放栈里,而是放在堆里。

逃逸不是坏事,只是让变量变成“堆对象”,让 GC 管理。性能影响是:堆=需要 GC,栈=不用 GC,所以堆分配更贵。

资源泄露和内存泄露的区别,跟 GC 没关系,不属于 Go 堆上的内存


Go 内存泄露:

  • goroutine 没退出,越积越多
  • map 不断累积数据,永远不删除:map 是强引用,你不删,GC 永远不能动它
  • slice 当队列用,但不清空前半部分

map 底层

  • Go 的 map 是分桶哈希表 + overflow 链 + 渐进扩容 + 随机迭代顺序的组合
  • map 循环是无序的

slice 的底层

slice 其实是一个结构体,里面只有三样东西:ptr len cap

  • ptr 底层数组的指针
  • len 可用的长度
  • cap 底层数组的容量

append 超过 cap 会触发扩容,Go 会新建一个更大的数组,把旧内容拷过去

gin 的工作原理

Gin 的工作原理就是 HTTP 请求 -> 封装 Context -> Radix Tree 路由匹配 -> 执行中间件链 -> 执行业务 Handler -> 写响应,同时通过 Context 池化和高效路由保证高性能

Gin 默认并不会自己维护 goroutine 池去复用协程,每个 HTTP 请求都是由 Go 的 net/http 直接在新的 goroutine 中执行的,由 Go runtime 的调度器管理(面试官还跟我说 gin 内部有协程池...)

go
func (srv *Server) Serve(l net.Listener) error {
   // ...
   ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, err := l.Accept()
        // ...
        connCtx := ctx
        // ...
        c := srv.newConn(rw)
        // ...
        go c.serve(connCtx)
    }
}

GPM 并发模型

  • 促进理解的几句话
    • Go runtime 把很多 goroutine 调度到少量线程上跑
    • 协程:用户态;线程:内核态
    • Go 在我看来核心优势是高并发模型。goroutine + channel 是用户态轻量调度,GPM 模型能高效利用 CPU
  • G(goroutine 队列)、P(执行单元)和 M(线程)的之间工作原理
  • 阻塞 syscall:磁盘 I/O 阻塞、网络链接阻塞、time.Sleep
  • 本地队列和全局队列
  • P 设置 GOMAXPROCS

sync.map 底层是如何实现的?

TODO

redis 分布式锁是公平锁吗?

TODO

go 的性能分析与调优

TODO

context 超时控制

耗时操作必须可取消,比如:

  • 网络请求 → 使用 http.NewRequestWithContext(ctx, ...)
  • 数据库查询 → 支持 context
  • 自己写循环耗时 → 每次循环检查 ctx.Done()
每个 goroutine 自己循环耗时,每次检查 ctx.Done() 的版本
go
package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    rand.Seed(time.Now().UnixNano())

    // 模拟创建任务
    tasks := make([]int, 20)
    for i := range tasks {
        tasks[i] = i + 1
    }

    maxConcurrency := 5
    sem := make(chan struct{}, maxConcurrency)

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    var wg sync.WaitGroup

    for _, task := range tasks {
        sem <- struct{}{}
        wg.Add(1)
        go func(t int) {
            defer wg.Done()
            defer func() { <-sem }()

            // 一个 for 循环任务,每次循环检查 ctx
            steps := rand.Intn(5) + 3
            for step := 1; step <= steps; step++ {
                select {
                case <-ctx.Done():
                    fmt.Printf("Task %d canceled at step %d\n", t, step)
                    return
                default:
                    // 模拟耗时操作
                    duration := time.Duration(rand.Intn(400)+100) * time.Millisecond
                    time.Sleep(duration)
                    fmt.Printf("Task %d step %d done, took %v\n", t, step, duration)
                }
            }

            fmt.Printf("Task %d fully done\n", t)
        }(task)
    }

    // 等待所有 goroutine 完成或 context 超时
    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
    }()

    select {
    case <-done:
        fmt.Println("All tasks completed")
    case <-ctx.Done():
        fmt.Println("Timeout reached, exiting")
    }
}

Go 内存模型

Go 内存模型定义了并发读写变量的可见性和顺序性,核心是 happens-before 原则。 只有通过 channel、mutex、atomic 等同步手段,才能保证 goroutine 之间看到一致的变量值。

内存逃逸和 GC

什么是内存逃逸(Escape):本来应该放在栈上的变量,被迫放到堆上了,这叫逃逸

为啥会逃逸:变量活得太久,栈装不下,只能去更持久的堆

发生逃逸的情况:

  • 函数返回局部变量的指针
  • interface 导致类型不确定 -> 为啥 -> 编译器无法确定变量类型大小,保险起见让它去堆上
  • append 导致切片扩容 -> 为啥 -> 栈内存装不下啊
  • 闭包 -> 为啥 -> 闭包还需访问函数的变量,变量无法随函数结束而消失
go
func foo() func() {
    x := 10
    return func() { fmt.Println(x) }
}

逃逸分析

逃逸分析决定变量放栈还是堆

逃逸分析一般对某个包、某个文件、甚至某段代码做局部分析,常用法是:

bash
go build -gcflags="-m" main.go

或通过 grep 定位关键输出,筛出性能敏感路径的堆分配点

bash
go build -gcflags="-m" file.go 2>&1 | grep escapes
举例说明

需要分析的 go 文件

go
package main

import "fmt"

func main() {
        foo()
        fmt.Println("Helloworld")
}

func foo() *int {
        x := 10
        return &x
}

执行逃逸分析命令

bash
=> go build -gcflags="-m" main.go
# command-line-arguments
./main.go:10:6: can inline foo
./main.go:6:5: inlining call to foo
./main.go:7:13: inlining call to fmt.Println
./main.go:7:13: ... argument does not escape
./main.go:7:14: "Helloworld" escapes to heap
./main.go:11:2: moved to heap: x

Go 逃逸分析核心知识总表

分类你需要关注的点示例输出含义(费曼式说明)优化方向
1. 变量逃逸到堆看到 escapes to heap 就重点看x escapes to heap这个变量不能随栈销毁,被放到堆或静态区了 → 会增加 GC 压力是否可改为栈变量、减少生命周期
2. 逃逸原因:取地址(&)taking address of x&x escapes to heap你把“钥匙地址”给出去了,必须放在堆不返回局部变量指针;用值传递
3. 逃逸原因:interface 封装assigned to interfacet escapes to heapinterface 像万能收纳箱,编译器不敢放栈避免频繁把小对象塞 interface
4. 逃逸原因:append 导致切片扩容append causes allocationmake([]int) escapes to heap桌子(栈)放不下,搬到院子(堆)预分配容量:make([]T, 0, n)
5. 逃逸原因:闭包捕获外部变量closure captures xx escapes to heap闭包会带着变量一起活,比函数活得久避免闭包引用大对象;或复制值
6. 字面量逃逸(字符串、map、slice)常见但无害"xxx" escapes to heap字面量放静态区,但分析器仍标为 escape一般忽略,不会频繁堆分配
7. 工具输出噪声:inline、ssa全部忽略can inline foo这与逃逸无关,是编译优化提示无需关注
8. 真正的定位方式只看含 escapes 的行grep escapes控制台噪声太多,只过滤关键点go build -gcflags="-m" file.go 2>&1 | grep escapes
9. 局部分析方式分析单个文件go build -gcflags="-m" file.go大项目输出巨大,只分析局部高效、干净、可重现
10. 深度分析-m -mgo build -gcflags="-m -m"输出更详细原因链路只在需要时使用

为啥做逃逸分析:帮助编译器尽量把变量分配到栈而不是堆,从而减少 GC 扫描次数。在高并发或性能敏感路径中,每次堆分配都可能触发 GC,逃逸分析减少堆分配 = 减少 GC 压力 = 系统更稳定高效

逃逸分析 -> GC

GC 优化关键词:

  • GC 爆炸
  • p99
  • STW

CPU 高 + STW 暂停 → 响应延迟飙升,系统吞吐下降

减少堆分配 → 减少 GC 频率 → 系统更稳 → 更高效

Go 的 GC 主要在堆增长超过阈值时触发,GC 会占用 CPU 并引起短暂暂停(STW);频繁堆分配会导致 GC 爆炸,表现为 CPU 占用高、延迟抖动、吞吐下降。减少堆分配、优化逃逸可以降低 GC 压力,从而让系统更稳定高效。

channel

  • 缓冲/无缓冲区别
  • channel 关闭后 send/receive 行为
    • 向已关闭 channel 发送会 panic
    • 从已关闭 channel 继续接收,不会阻塞,永远读到零值
    • 判断 channel 是否关闭只能通过 “comma, ok” 第二个 bool 参数

select 的调度策略

TODO select 的调度策略(随机、一致性)

情况行为
多个 case 都可执行随机选一个(不会固定某个 case)
有 default若所有 case 都阻塞,立刻走 default
没有 default所有 case 都阻塞则阻塞整个 select
select 实现超时time.After() 会创建定时器 channel

后端系统设计

  • 高并发怎么扛:负载均衡、缓存、集群
  • 缓存怎么布置
  • 数据一致性怎么保障:强一致(ACID)、最终一致(BASE)
  • 限流、熔断、降级怎么做

Redis 使用场景

  • 缓存(Cache Aside)
    • 缓存问题:缓存穿透、缓存击穿、缓存雪崩
    • 缓存更新
    • 缓存过期 LRU
  • 分布式锁(SET NX EX)
  • 限流(滑动窗口 / token bucket)

可观测意识:日志 / 指标 / 链路追踪

日志:

  • 库:log、zap
  • 日志分级(info/debug/warn/error)
  • request_id
  • 日志结构化 JSON -> 阿里云 SLS

指标:

Prometheus + Grafana 没用过

链路追踪:

OpenTelemetry / Jaeger

  • 上下游传递 traceID,支持 span / parent-child 层级
  • 如何在 RPC 或 HTTP 微服务中传递 traceID

我会用结构化日志 + traceID 进行链路追踪,同时通过 Prometheus/Grafana 监控关键指标,保证线上可快速定位问题

性能分析与调优

  • pprof
    • 类似 linux 的 top 命令
    • pprof 是 Go 内置性能分析工具,支持 CPU、内存、goroutine、阻塞
    • 帮你找出消耗资源最多的函数
  • 火焰图(Flame Graph):pprof 输出可转成火焰图,直观显示 CPU 时间分布
    • CPU 消耗最多的函数在哪里?
    • 怎么优化热点函数?
  • GC 调优
    • Go GC 特点:并发标记清扫(concurrent mark-sweep)、STW pause 短暂暂停
    • 触发因素:堆分配过多、大对象频繁分配
    • 调优方法:避免逃逸、减少短生命周期对象堆分配

用 pprof CPU / heap / block profile 定位热点函数和内存泄漏,再用火焰图分析消耗,最后结合逃逸分析和堆分配优化 GC


高并发 RPC 请求延迟高 → pprof 找到热点在 JSON 序列化 → 用 jsoniter 替换 → CPU 消耗下降 30% 内存暴涨 → heap profile → 大对象频繁堆分配 → 改为对象池复用 → GC pause 明显下降

grpc 的主要响应模式

TODO

defer 的执行逻辑

TODO

你用过哪些 mcp?

  • playwright
  • github
  • context7
  • n8n
  • markitdown