写点什么

假期结束!上上强度!

作者:王中阳Go
  • 2025-05-06
    湖南
  • 本文字数:3724 字

    阅读完需:约 12 分钟

时间过得真快啊,一眨眼假期就结束了,五天说长不长说短不短,不知道大家有没有出去旅游?还是呆在家里休息?(不会在公司加班吧??!!)


我反正是出去玩了一顿,不出去走走放松一下心情真的很容易把人憋坏


正好,最近组织内的朋友给我分享了他在同程旅行一、二面的面经,想必大家放假的时候在学习上也懈怠了,我来给你们上上强度。


面经详解

一面

1. 介绍下什么是 #大对象小对象 #阈值是多少 区别在哪里 内存分配有什么不一样

大对象小对象的划分是基于内存分配的粒度和策略。Go 的运行时系统(runtime)对不同大小的对象采用不同的内存分配方式,以优化性能和减少内存碎片。


  • 阈值

  • 小对象的阈值是 16B32KB,而 大对象 是指大小超过 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
复制代码


  1. 极端情况:如果扩容后仍不足,直接分配所需容量。


  • 扩容过程

  • 分配新的底层数组(大小为newCap)。

  • 将旧数组的数据复制到新数组。

  • 更新 slice 的ptr(指向新数组)、len(长度)和cap(容量)。



3. 有缓冲 Channel 关闭后接受发送

有缓冲 Channel(buffered channel)关闭后:


  • 发送操作

  • 如果 Channel 已关闭,尝试发送数据会触发 panic(运行时错误)。

  • 即使 Channel 仍有剩余容量,关闭后也不能再发送数据。

  • 接收操作

  • 关闭后仍可以接收 Channel 中剩余的数据。

  • 当所有数据被接收后,后续接收操作会返回零值(zero value),并标记okfalse



4. 限流算法了解哪些

常见的限流算法包括以下几种:


  1. 令牌桶算法(Token Bucket)

  2. 原理:维护一个令牌桶,以固定速率向桶中添加令牌。请求需要消耗令牌,若无可用令牌则被拒绝。

  3. 优点:允许突发流量(令牌桶可积攒令牌)。

  4. 缺点:实现复杂度较高。

  5. 漏桶算法(Leaky Bucket)

  6. 原理:以固定速率处理请求,超出容量的请求会被丢弃或排队。

  7. 优点:平滑流量,防止突发流量冲击。

  8. 缺点:无法应对突发流量。

  9. 固定窗口计数器(Fixed Window Counter)

  10. 原理:在固定时间窗口内统计请求数量,超过阈值则限流。

  11. 缺点:存在临界点问题(如窗口边界处的请求突增)。

  12. 滑动窗口计数器(Sliding Window Counter)

  13. 原理:动态统计最近一段时间内的请求数量,解决固定窗口的临界问题。

  14. 优点:更精确地控制流量。

  15. 信号量(Semaphore)

  16. 原理:通过控制并发资源的数量实现限流。

  17. 优点:简单易用,适合资源池管理。



5. 说下令牌算法漏桶算法的优缺点

令牌桶算法漏桶算法是两种经典的限流策略,它们的优缺点如下:



  • 令牌桶算法

  • 优点:允许突发流量,灵活性高。

  • 缺点:实现复杂,需维护令牌数量和时间。

  • 漏桶算法

  • 优点:平滑流量,防止系统过载。

  • 缺点:无法应对突发流量,可能造成请求堆积。



6. redis 怎么实现队列

Redis 可以通过其 List 数据结构实现队列,具体方法如下:


  1. 基础队列

  2. 入队:使用RPUSH命令将元素添加到队列尾部。

  3. 出队:使用LPOP命令从队列头部取出元素。

  4. 阻塞队列

  5. 阻塞出队:使用BLPOPBRPOP命令,若队列为空则阻塞等待。

  6. 优先级队列

  7. 使用ZSET(有序集合)实现优先级队列,通过分数(score)控制优先级。

  8. 分布式队列

  9. 结合 Redis 的分布式锁(如SETNX)实现多节点协作。



7. jwt 和传统 cookie、session 的区别


  • 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 因未正确退出而持续占用资源,导致内存泄漏或资源耗尽。


  • 常见原因


  1. Channel 未关闭

  2. 死循环未退出

  3. WaitGroup 未释放


  • 解决方案

  • 使用 context.Context 控制 Goroutine 生命周期。

  • 确保 Channel 在不再使用时关闭。

  • 为死循环添加退出条件(如超时或外部信号)。



3. 三次握手四次挥手 为解决什么问题

三次握手四次挥手是 TCP 协议建立和终止连接的核心机制,用于解决以下问题:


  • 三次握手(建立连接)

  • 目的:确保双方都能正常收发数据,避免因重复连接请求导致混乱。

  • 流程

  • SYN:客户端发送连接请求。

  • SYN-ACK:服务器确认请求并回复。

  • ACK:客户端确认连接成功。

  • 四次挥手(终止连接)

  • 目的:确保双方完成数据传输后安全关闭连接,避免半开连接。

  • 流程

  • FIN:主动关闭方发送结束请求。

  • ACK:被动关闭方确认。

  • FIN:被动关闭方发送结束请求。

  • ACK:主动关闭方确认。

  • 关键问题

  • 避免重复连接(三次握手)。

  • 确保数据完整性(四次挥手)。



4. 粘包 原理及解决

粘包(Sticky Packet)是 TCP 协议中因流式传输导致的数据边界模糊问题。


  • 原理

  • TCP 是流式协议,不维护消息边界。发送端连续发送的数据可能被接收端合并为一个或多个数据包。

  • 示例:发送两个消息"ABC""DEF",接收端可能收到"ABCDEF"(粘包)。

  • 解决方法


  1. 固定长度:每条消息固定长度(不足时填充空字符)。


     // 发送端     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]))
复制代码


  1. 自定义协议头:在消息前添加长度字段。


     // 发送端     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)
复制代码


  1. 特殊分隔符:使用特定字符(如\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)
复制代码


  1. 动态分配内存:根据长度字段分配缓冲区。


     data := make([]byte, length)     conn.Read(data)
复制代码


  1. 预分配缓冲区:对于固定长度的消息,预先分配固定大小的缓冲区。


     buffer := make([]byte, 1024) // 假设最大消息长度为1024     n, _ := conn.Read(buffer)
复制代码


  • 注意事项

  • 避免缓冲区溢出:限制最大消息长度(如MAX_PACKET_SIZE)。

  • 处理分片:如果数据包分多次到达,需缓存未接收部分。

  • 校验数据完整性:通过校验和或 CRC 验证数据是否完整。

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。


没准能让你能刷到自己意向公司的最新面试题呢。


感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。

发布于: 刚刚阅读数: 4
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
假期结束!上上强度!_go面试题_王中阳Go_InfoQ写作社区