GO面试要点笔记(持续更新)

第一周

image-20260620101531624

1.问题回答

1. 什么是闭包?闭包有什么缺陷?

  • 什么是闭包:闭包是指一个函数(在 Go 中通常指匿名函数)引用了其外部作用域中的变量。即使该外部函数已经返回,这个匿名函数依然可以访问并修改这些外部变量。
  • 缺陷(注意事项)
    1. 内存泄漏:如果闭包长期存活(例如作为全局变量或长期运行的协程),其捕获的外部变量将一直无法被 GC 回收,导致内存占用过高。
    2. 共享变量副作用:在循环中使用闭包非常容易出错。例如 for i:=0; i<10; i++ { go func() { println(i) }() } 会打印多个 10,因为所有闭包共享了同一个变量 i
    3. 性能开销:闭包本质上是通过指针访问外部变量,底层涉及逃逸分析,有一定的性能开销。

2. 什么情况下会出现栈溢出?

  • 无限递归:函数直接或间接地无限调用自身,导致调用栈无限堆积。
  • 分配过大的局部变量:在函数内部声明了非常大且不能逃逸到堆上的数组或结构体,导致单个栈帧过大。
  • Go 特定情况:Go 的协程(Goroutine)初始栈很小(约 2KB),但会自动扩容。虽然 Go 的栈是动态调整的,但如果递归极深或单帧极大,导致扩容申请失败时,依然会出现 stack overflow 导致程序崩溃。

3. 什么是不定参数?调用时能传 0 个值吗?方法内部怎么使用?

  • 什么是不定参数:函数最后一个参数前加上 ... 符号(如 func Sum(nums ...int)),表示该参数可接受 0 个或多个同类型的参数。
  • 能否传 0 个值可以。如果不传值,在函数内部得到的 nums 会是一个空切片(长度为 0,容量为 0)。
  • 内部使用方法:在方法内部,不定参数实际上是被作为一个切片[]T)来处理的。你可以使用 range 遍历它,也可以直接进行切片操作。如果你需要将其传给另一个不定参数函数,需要采用 nums... 的形式来解包。

4. 什么是 defer?你能解释一下 defer 的运作机制吗?

  • 什么是 deferdefer 语句用于延迟执行一个函数调用,被推迟的函数会在所在函数返回之前(无论是否发生 panic)执行。
  • 运作机制
    1. 后进先出(LIFO):如果函数内有多个 defer,它们按照声明的相反顺序执行(类似栈)。
    2. 参数立即求值defer 后面的函数参数(除了函数体内部的变量)会在声明 defer 的那一刻立即求值并拷贝保存,而不是在 defer 真正执行时才去求值。
    3. 数据结构:在 Go 的运行时中,defer 会被串联成一个链表挂在 Goroutine 结构体上。编译时会根据情况选择不同的实现(如堆分配、栈分配或开放编码)。

5. 一个方法内部 defer 能不能超过 8 个?

  • 。语法和运行机制上没有硬性代码限制,你可以写 8 个以上甚至 100 个。
  • 深入理解(性能优化):这个问题通常考察 Go 1.14 之后的编译优化。在 Go 1.14+ 中,如果 defer 数量不超过 8 个,且不是在循环中调用,编译器会采用**“开放编码” (Open-Coded Defer)**,这是一种非常高效的内联实现。一旦超过 8 个,defer 就会在运行时退化为栈或堆上的链表执行,性能会有所下降,但功能完全正常。

6. defer 内部能不能修改返回值?怎么改?

  • 能修改,但取决于返回值的声明方式
  • 情况 1(匿名返回值):如果函数返回值是匿名的(如 func f() int),defer 无法直接修改返回值(因为匿名返回值在函数栈中,defer 无法拿到它的地址)。除非你在 defer 里操作的是全局变量或指针类型的返回值。
  • 情况 2(具名返回值):如果函数返回值是具名的(如 func f() (result int)),defer 可以直接修改 result 的值,因为 defer 可以访问到该命名返回变量的内存地址。最终返回的是 defer 修改后的值。

7. 数组和切片有什么区别?

  • 长度
    • 数组:长度是固定的,且是数组类型定义的一部分(如 [5]int[10]int 是不同类型)。
    • 切片:长度是动态的,可以扩容。
  • 传参
    • 数组:作为参数传递时是值拷贝(整个数组都会复制一份)。
    • 切片:作为参数传递时是引用传递(传递的是指向底层数组的指针、长度和容量的结构体副本)。
  • 适用场景:底层存储为数组,日常开发中极少使用数组作为参数传递,绝大多数情况使用切片。

8. 切片怎么扩容的?

使用 append 函数向切片追加元素时,如果切片的容量(cap)不足以容纳新元素,就会触发扩容机制。

  • 扩容策略
    1. 需求容量:如果追加后的新长度 > 当前容量的 2 倍,则新容量直接设为新长度
    2. 2倍扩容:如果当前切片长度小于 1024,则新容量翻倍(cap * 2)。
    3. 1.25倍扩容:如果当前切片长度大于等于 1024,则新容量增长为原来的 1.25 倍
  • 补充细节:扩容并不是仅仅数学上乘以一个倍数,Go 在确定上述估算值后,还会进行内存对齐操作,对最终分配的内存块大小进行向上取整,以节省内存并适应计算机的缓存和内存分配策略。
  • 注意:扩容会重新分配底层数组。如果发生扩容,新切片和旧切片将不再共享底层数组

2.defer相关

image-20260620103744006

image-20260620104055822

image-20260620104156602

image-20260620104355535

3.切片扩容

image-20260620104413752

image-20260620104503538

回来看的时候记得看一下最新版本的go是怎么扩容的,就看看还是不是256,1.25之类的,这个经常改动

第二周

image-20260621105238593

image-20260621105252404

这张幻灯片里提到的三个问题,确实是 Gin 面试中最基础、最容易被考察的核心知识点。我帮你把这三个问题,连同底部那条“黄金面试建议”一并详细解答出来:


1. 什么是 Gin 的 middleware?能用来解决什么问题?

(核心:拦截器)

  • 定义:Middleware(中间件)本质上是一个函数,它在 HTTP 请求到达具体的业务处理函数(Handler)之前,以及处理完业务返回响应之后,插入执行的一段逻辑。它组成了一个“洋葱圈”的调用链。
  • 能解决的问题(应用场景)
    • 全局限流与防护:限制某个 IP 的请求频率,防止 DDOS 攻击。
    • 鉴权与认证:统一校验请求头里的 Token 或 Cookie,如果没登录,直接在中间件里拦截并返回 401,不让请求触达真实的业务接口。
    • 请求日志与追踪:记录每一个接口的耗时、请求参数、返回状态码,便于排查线上问题。
    • 统一异常处理:捕获业务代码中可能发生的 panic,防止服务直接崩溃,并返回统一的错误格式给前端。
    • 跨域处理(CORS):下面要讲到的跨域问题,通常就是通过中间件解决的。
  • 在 Gin 中怎么用:非常简单,直接调用 r.Use(MyMiddleware()) 就可以把中间件挂载到全局或特定的路由组上。

2. 什么是跨域问题,怎么解决?

(核心:浏览器同源策略)

  • 定义:为了安全,浏览器有一个同源策略。如果你的前端运行在 http://localhost:8080,但你请求的后端接口在 http://localhost:3000(只要协议、域名、端口有一个不同,就算跨域),浏览器就会拦下这个请求,直接报错。
  • 怎么解决(三种主要方式)
    1. 后端加 CORS 响应头(最常用):让后端告诉浏览器:“我是值得信任的,我允许前端跨域来访问我”。在 Gin 中,最推荐的做法是直接使用官方的 gin-contrib/cors 中间件包,配置极简。
    2. 前端代理:在开发阶段,使用 Webpack、Vite 等前端构建工具配置 proxy 代理,把对前端的请求转发给后端,因为“服务器与服务器之间”是没有跨域限制的。
    3. Nginx 反向代理:在生产环境,把前端和后端托管在同一个域名的不同路径下,比如 www.example.com/api 和后端,www.example.com 和前端,通过 Nginx 代理消除跨域。

3. 跨域问题需要设置哪些头部?

在 Gin 后端(或其他语言后端)解决跨域时,实质上就是在 HTTP 响应头里添加以下关键字段

  • Access-Control-Allow-Origin必须):明确告诉浏览器允许哪些域名来跨域访问。如果前端地址不固定,通常可以读取请求头里的 Origin 动态返回。注意:如果包含 Authorization 头部,这里不能写 *,必须写死具体的域名。
  • Access-Control-Allow-Methods:允许前端的请求方法,比如 GET, POST, PUT, DELETE, OPTIONS
  • Access-Control-Allow-Headers:允许前端在请求头里携带自定义的 Header,例如 Content-Type, Authorization, X-Requested-With 等。
  • Access-Control-Allow-Credentials(可选):如果前端请求携带了 Cookie,需要把这个值设为 true
  • Access-Control-Max-Age(预检缓存):浏览器在发出复杂请求前,会先发一个 OPTIONS 预检请求。设置这个头部(例如 86400 秒),可以让浏览器在一天内无需再次发送 OPTIONS 探测,直接发送真实请求,能显著提升性能。

额外补充:幻灯片底部的“黄金面试建议”

在 Gin 面试的时候,一定要提起自己研发了一个强大 Gin 插件库”,这句话非常有深意。它想考察的是你有没有读懂 Gin 的底层设计思想

你可以这么说:

“除了会使用中间件外,我对 Gin 的洋葱模型(Next() 机制)有深入的理解。基于这种机制,我在工作中提取并封装了一套公司内部通用的 Gin 插件库。比如:

  1. 基于 Redis + 令牌桶算法实现的全局限流中间件
  2. 统一处理 panic 和业务异常,将 error 转换为标准 JSON 结构体返回的中间件;
  3. 基于 pprof 做了个自定义的性能监控中间件,并且给公司内部的其他 Go 微服务框架复用。

熟练封装中间件的能力,能体现出我对 Gin 底层链式调用源码的理解。”

这样回答,展现了具备模块化封装、业务落地、和源码深度的能力,会是非常大的加分项!