时间过得真快啊,一眨眼假期就结束了,五天说长不长说短不短,不知道大家有没有出去旅游?还是呆在家里休息?(不会在公司加班吧??!!)
我反正是出去玩了一顿,不出去走走放松一下心情真的很容易把人憋坏。
正好,最近组织内的朋友给我分享了他在同程旅行一、二面的面经,想必大家放假的时候在学习上也懈怠了,我来给你们上上强度。
面经详解
一面
1. 介绍下什么是 #大对象小对象 #阈值是多少 区别在哪里 内存分配有什么不一样
大对象和小对象的划分是基于内存分配的粒度和策略。Go的运行时系统(runtime)对不同大小的对象采用不同的内存分配方式,以优化性能和减少内存碎片。
阈值
小对象的阈值是 16B 到 32KB,而 大对象 是指大小超过 32KB 的对象。- 小对象(16B~32KB):由Go的mcache(线程缓存)分配。
- 大对象(>32KB):直接从堆(heap)中分配,绕过mcache和mcentral,以避免碎片化。
区别
分配方式
- 小对象:通过mcache(线程级缓存)分配,每个P(逻辑处理器)对应一个mcache,分配速度快,减少锁竞争。
- 大对象:直接从堆分配,由Go的mheap(全局堆)管理,分配时可能触发GC(垃圾回收)或调整堆大小。
内存碎片
- 小对象容易产生内存碎片,因为频繁分配和释放可能导致空闲内存块分散。
- 大对象通常一次性分配较大的连续内存块,减少碎片化风险。
性能优化
- 小对象的分配和回收效率较高,适合频繁创建和销毁的场景(如临时变量)。
- 大对象的分配成本较高,适合生命周期较长的对象(如大数组、大结构体)。
内存分配策略
Go通过内存分级管理(mcache → mcentral → mheap)优化内存分配:- mcache:线程本地缓存,快速分配小对象。
- mcentral:全局缓存,管理多个mcache的内存块,协调小对象分配。
- mheap:全局堆,负责大对象分配和堆管理。
2. slice的扩容机制
slice(切片)是一种动态数组,其底层依赖于数组和长度/容量信息。当slice的容量不足时,会触发扩容机制,具体规则如下:
扩容规则
- 容量小于256:新容量为原容量的两倍。
容量大于等于256:新容量会采用逐步增长策略,减少过度分配。
newCap = oldCap + (3 * 256 + oldCap) / 4
- 极端情况:如果扩容后仍不足,直接分配所需容量。
扩容过程
- 分配新的底层数组(大小为
newCap
)。 - 将旧数组的数据复制到新数组。
- 更新slice的
ptr
(指向新数组)、len
(长度)和cap
(容量)。
- 分配新的底层数组(大小为
3. 有缓冲Channel 关闭后接受发送
有缓冲Channel(buffered channel)关闭后:
发送操作
- 如果Channel已关闭,尝试发送数据会触发panic(运行时错误)。
- 即使Channel仍有剩余容量,关闭后也不能再发送数据。
接收操作
- 关闭后仍可以接收Channel中剩余的数据。
- 当所有数据被接收后,后续接收操作会返回零值(zero value),并标记
ok
为false
。
4. 限流算法了解哪些
常见的限流算法包括以下几种:
令牌桶算法(Token Bucket)
- 原理:维护一个令牌桶,以固定速率向桶中添加令牌。请求需要消耗令牌,若无可用令牌则被拒绝。
- 优点:允许突发流量(令牌桶可积攒令牌)。
- 缺点:实现复杂度较高。
漏桶算法(Leaky Bucket)
- 原理:以固定速率处理请求,超出容量的请求会被丢弃或排队。
- 优点:平滑流量,防止突发流量冲击。
- 缺点:无法应对突发流量。
固定窗口计数器(Fixed Window Counter)
- 原理:在固定时间窗口内统计请求数量,超过阈值则限流。
- 缺点:存在临界点问题(如窗口边界处的请求突增)。
滑动窗口计数器(Sliding Window Counter)
- 原理:动态统计最近一段时间内的请求数量,解决固定窗口的临界问题。
- 优点:更精确地控制流量。
信号量(Semaphore)
- 原理:通过控制并发资源的数量实现限流。
- 优点:简单易用,适合资源池管理。
5. 说下令牌算法漏桶算法的优缺点
令牌桶算法和漏桶算法是两种经典的限流策略,它们的优缺点如下:
特性 | 令牌桶算法 | 漏桶算法 |
---|---|---|
突发流量支持 | 支持(令牌桶可积攒令牌) | 不支持(固定速率处理) |
实现复杂度 | 较高(需维护令牌数量和时间) | 较低(只需维护请求队列和处理速率) |
响应延迟 | 低(突发流量可立即处理) | 高(请求需排队等待) |
适用场景 | 需要应对突发流量的场景(如API网关) | 需要严格平滑流量的场景(如网络带宽) |
令牌桶算法
- 优点:允许突发流量,灵活性高。
- 缺点:实现复杂,需维护令牌数量和时间。
漏桶算法
- 优点:平滑流量,防止系统过载。
- 缺点:无法应对突发流量,可能造成请求堆积。
6. redis怎么实现队列
Redis可以通过其List数据结构实现队列,具体方法如下:
基础队列
- 入队:使用
RPUSH
命令将元素添加到队列尾部。 - 出队:使用
LPOP
命令从队列头部取出元素。
- 入队:使用
阻塞队列
- 阻塞出队:使用
BLPOP
或BRPOP
命令,若队列为空则阻塞等待。
- 阻塞出队:使用
优先级队列
- 使用
ZSET
(有序集合)实现优先级队列,通过分数(score)控制优先级。
- 使用
分布式队列
- 结合Redis的分布式锁(如
SETNX
)实现多节点协作。
- 结合Redis的分布式锁(如
7. jwt和传统cookie、session的区别
特性 | JWT | Cookie-Session |
---|---|---|
状态保存 | 无状态(服务器无需存储会话信息) | 有状态(服务器需存储Session数据) |
跨域支持 | 支持(Token可携带至任意域名) | 受同源策略限制(Cookie绑定域名) |
扩展性 | 高(适合分布式系统) | 低(Session需集中存储) |
安全性 | 需加密签名(防止篡改) | 依赖Cookie的安全属性(HttpOnly等) |
传输方式 | 通过Header(Authorization)或参数传递 | 通过Cookie自动传递 |
JWT的适用场景
- 分布式系统(微服务架构)。
- 跨域认证(如SPA应用)。
传统Session的适用场景
- 单体应用或小型系统。
- 需要高安全性的场景(如银行系统)。
8. 说一下etcd
etcd是一个高可用的分布式键值存储系统,主要用于服务发现、配置管理、分布式锁等场景。
核心特性
- 强一致性:基于Raft协议实现分布式一致性。
- 高可用性:支持集群部署,容忍节点故障。
- 简单API:提供RESTful接口和Go客户端库。
应用场景
- 服务注册与发现:存储服务实例的元数据。
- 分布式锁:通过租约实现锁的自动释放。
- 配置中心:集中管理分布式系统的配置。
二面
1. gc
Go语言的垃圾回收(GC)机制是基于标记-清除(Mark and Sweep)算法的并发垃圾回收器,主要特点如下:
GC流程
- 标记阶段:从根对象(全局变量、栈变量等)出发,遍历所有可达对象,并标记为“存活”。
- 清除阶段:回收未被标记的对象(垃圾)。
并发性
- Go的GC是并发执行的,与程序逻辑线程(M)并行运行,减少程序暂停时间。
- 使用三色标记法(白色、灰色、黑色)管理对象状态,避免标记错误。
低延迟设计
- 通过分代回收(Generational GC)优化,优先回收短生命周期对象。
- 写屏障(Write Barrier)确保标记阶段的准确性。
性能优化
- STW(Stop-The-World):仅在标记阶段的初始和结束阶段短暂暂停程序。
- 内存分配:使用mcache/mcentral/mheap分级管理内存,减少GC压力。
2. 协程泄露
协程泄露(Goroutine Leak)是指Goroutine因未正确退出而持续占用资源,导致内存泄漏或资源耗尽。
- 常见原因
- Channel未关闭:
- 死循环未退出:
- WaitGroup未释放:
解决方案
- 使用context.Context控制Goroutine生命周期。
- 确保Channel在不再使用时关闭。
- 为死循环添加退出条件(如超时或外部信号)。
3. 三次握手四次挥手 为解决什么问题
三次握手和四次挥手是TCP协议建立和终止连接的核心机制,用于解决以下问题:
三次握手(建立连接)
- 目的:确保双方都能正常收发数据,避免因重复连接请求导致混乱。
流程:
- SYN:客户端发送连接请求。
- SYN-ACK:服务器确认请求并回复。
- ACK:客户端确认连接成功。
四次挥手(终止连接)
- 目的:确保双方完成数据传输后安全关闭连接,避免半开连接。
流程:
- FIN:主动关闭方发送结束请求。
- ACK:被动关闭方确认。
- FIN:被动关闭方发送结束请求。
- ACK:主动关闭方确认。
关键问题
- 避免重复连接(三次握手)。
- 确保数据完整性(四次挥手)。
4. 粘包 原理及解决
粘包(Sticky Packet)是TCP协议中因流式传输导致的数据边界模糊问题。
原理
- TCP是流式协议,不维护消息边界。发送端连续发送的数据可能被接收端合并为一个或多个数据包。
- 示例:发送两个消息
"ABC"
和"DEF"
,接收端可能收到"ABCDEF"
(粘包)。
- 解决方法
固定长度:每条消息固定长度(不足时填充空字符)。
// 发送端 data := []byte("ABC") paddedData := make([]byte, 10) copy(paddedData, data) conn.Write(paddedData) // 接收端 buffer := make([]byte, 10) n, _ := conn.Read(buffer) fmt.Println(string(buffer[:n]))
自定义协议头:在消息前添加长度字段。
// 发送端 header := make([]byte, 4) binary.BigEndian.PutUint32(header, uint32(len(data))) conn.Write(append(header, data...)) // 接收端 header := make([]byte, 4) conn.Read(header) length := binary.BigEndian.Uint32(header) data := make([]byte, length) conn.Read(data)
特殊分隔符:使用特定字符(如
\n
)分隔消息。// 发送端 conn.Write([]byte("ABC\nDEF\n")) // 接收端 reader := bufio.NewReader(conn) for { line, _ := reader.ReadString('\n') fmt.Println(line) }
5. 自定义一个协议,怎么知道要分配多大一个内存去存放接收的数据包
在自定义协议中,确定接收数据包的内存分配策略需要结合协议设计:
步骤
解析协议头:在协议头中包含数据长度字段(如4字节的BigEndian整数)。
// 协议格式:[4字节长度][数据] header := make([]byte, 4) conn.Read(header) length := binary.BigEndian.Uint32(header)
动态分配内存:根据长度字段分配缓冲区。
data := make([]byte, length) conn.Read(data)
预分配缓冲区:对于固定长度的消息,预先分配固定大小的缓冲区。
buffer := make([]byte, 1024) // 假设最大消息长度为1024 n, _ := conn.Read(buffer)
注意事项
- 避免缓冲区溢出:限制最大消息长度(如
MAX_PACKET_SIZE
)。 - 处理分片:如果数据包分多次到达,需缓存未接收部分。
- 校验数据完整性:通过校验和或CRC验证数据是否完整。
- 避免缓冲区溢出:限制最大消息长度(如
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。