头图

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

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

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

面经详解

一面

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

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

  • 阈值
    小对象的阈值是 16B32KB,而 大对象 是指大小超过 32KB 的对象。

    • 小对象(16B~32KB):由Go的mcache(线程缓存)分配。
    • 大对象(>32KB):直接从(heap)中分配,绕过mcache和mcentral,以避免碎片化。
  • 区别

    1. 分配方式

      • 小对象:通过mcache(线程级缓存)分配,每个P(逻辑处理器)对应一个mcache,分配速度快,减少锁竞争。
      • 大对象:直接从堆分配,由Go的mheap(全局堆)管理,分配时可能触发GC(垃圾回收)或调整堆大小。
    2. 内存碎片

      • 小对象容易产生内存碎片,因为频繁分配和释放可能导致空闲内存块分散。
      • 大对象通常一次性分配较大的连续内存块,减少碎片化风险。
    3. 性能优化

      • 小对象的分配和回收效率较高,适合频繁创建和销毁的场景(如临时变量)。
      • 大对象的分配成本较高,适合生命周期较长的对象(如大数组、大结构体)。
  • 内存分配策略
    Go通过内存分级管理(mcache → mcentral → mheap)优化内存分配:

    1. mcache:线程本地缓存,快速分配小对象。
    2. mcentral:全局缓存,管理多个mcache的内存块,协调小对象分配。
    3. mheap:全局堆,负责大对象分配和堆管理。

2. slice的扩容机制

slice(切片)是一种动态数组,其底层依赖于数组长度/容量信息。当slice的容量不足时,会触发扩容机制,具体规则如下:

  • 扩容规则

    1. 容量小于256:新容量为原容量的两倍
    2. 容量大于等于256:新容量会采用逐步增长策略,减少过度分配。

      newCap = oldCap + (3 * 256 + oldCap) / 4
    3. 极端情况:如果扩容后仍不足,直接分配所需容量。
  • 扩容过程

    1. 分配新的底层数组(大小为newCap)。
    2. 将旧数组的数据复制到新数组。
    3. 更新slice的ptr(指向新数组)、len(长度)和cap(容量)。

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

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

  • 发送操作

    • 如果Channel已关闭,尝试发送数据会触发panic(运行时错误)。
    • 即使Channel仍有剩余容量,关闭后也不能再发送数据。
  • 接收操作

    • 关闭后仍可以接收Channel中剩余的数据。
    • 当所有数据被接收后,后续接收操作会返回零值(zero value),并标记okfalse

4. 限流算法了解哪些

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

  1. 令牌桶算法(Token Bucket)

    • 原理:维护一个令牌桶,以固定速率向桶中添加令牌。请求需要消耗令牌,若无可用令牌则被拒绝。
    • 优点:允许突发流量(令牌桶可积攒令牌)。
    • 缺点:实现复杂度较高。
  2. 漏桶算法(Leaky Bucket)

    • 原理:以固定速率处理请求,超出容量的请求会被丢弃或排队。
    • 优点:平滑流量,防止突发流量冲击。
    • 缺点:无法应对突发流量。
  3. 固定窗口计数器(Fixed Window Counter)

    • 原理:在固定时间窗口内统计请求数量,超过阈值则限流。
    • 缺点:存在临界点问题(如窗口边界处的请求突增)。
  4. 滑动窗口计数器(Sliding Window Counter)

    • 原理:动态统计最近一段时间内的请求数量,解决固定窗口的临界问题。
    • 优点:更精确地控制流量。
  5. 信号量(Semaphore)

    • 原理:通过控制并发资源的数量实现限流。
    • 优点:简单易用,适合资源池管理。

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

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

特性令牌桶算法漏桶算法
突发流量支持支持(令牌桶可积攒令牌)不支持(固定速率处理)
实现复杂度较高(需维护令牌数量和时间)较低(只需维护请求队列和处理速率)
响应延迟低(突发流量可立即处理)高(请求需排队等待)
适用场景需要应对突发流量的场景(如API网关)需要严格平滑流量的场景(如网络带宽)
  • 令牌桶算法

    • 优点:允许突发流量,灵活性高。
    • 缺点:实现复杂,需维护令牌数量和时间。
  • 漏桶算法

    • 优点:平滑流量,防止系统过载。
    • 缺点:无法应对突发流量,可能造成请求堆积。

6. redis怎么实现队列

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

  1. 基础队列

    • 入队:使用RPUSH命令将元素添加到队列尾部。
    • 出队:使用LPOP命令从队列头部取出元素。
  2. 阻塞队列

    • 阻塞出队:使用BLPOPBRPOP命令,若队列为空则阻塞等待。
  3. 优先级队列

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

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

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

特性JWTCookie-Session
状态保存无状态(服务器无需存储会话信息)有状态(服务器需存储Session数据)
跨域支持支持(Token可携带至任意域名)受同源策略限制(Cookie绑定域名)
扩展性高(适合分布式系统)低(Session需集中存储)
安全性需加密签名(防止篡改)依赖Cookie的安全属性(HttpOnly等)
传输方式通过Header(Authorization)或参数传递通过Cookie自动传递
  • JWT的适用场景

    • 分布式系统(微服务架构)。
    • 跨域认证(如SPA应用)。
  • 传统Session的适用场景

    • 单体应用或小型系统。
    • 需要高安全性的场景(如银行系统)。

8. 说一下etcd

etcd是一个高可用的分布式键值存储系统,主要用于服务发现、配置管理、分布式锁等场景。

  • 核心特性

    1. 强一致性:基于Raft协议实现分布式一致性。
    2. 高可用性:支持集群部署,容忍节点故障。
    3. 简单API:提供RESTful接口和Go客户端库。
  • 应用场景

    • 服务注册与发现:存储服务实例的元数据。
    • 分布式锁:通过租约实现锁的自动释放。
    • 配置中心:集中管理分布式系统的配置。

二面

1. gc

Go语言的垃圾回收(GC)机制是基于标记-清除(Mark and Sweep)算法的并发垃圾回收器,主要特点如下:

  • GC流程

    1. 标记阶段:从根对象(全局变量、栈变量等)出发,遍历所有可达对象,并标记为“存活”。
    2. 清除阶段:回收未被标记的对象(垃圾)。
  • 并发性

    • 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协议建立和终止连接的核心机制,用于解决以下问题:

  • 三次握手(建立连接)

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

      • SYN:客户端发送连接请求。
      • SYN-ACK:服务器确认请求并回复。
      • ACK:客户端确认连接成功。
  • 四次挥手(终止连接)

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

      • 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]))
  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)
  2. 特殊分隔符:使用特定字符(如\n)分隔消息。

      // 发送端
      conn.Write([]byte("ABC\nDEF\n"))
    
      // 接收端
      reader := bufio.NewReader(conn)
      for {
          line, _ := reader.ReadString('\n')
          fmt.Println(line)
      }

5. 自定义一个协议,怎么知道要分配多大一个内存去存放接收的数据包

在自定义协议中,确定接收数据包的内存分配策略需要结合协议设计:

  • 步骤

    1. 解析协议头:在协议头中包含数据长度字段(如4字节的BigEndian整数)。

      // 协议格式:[4字节长度][数据]
      header := make([]byte, 4)
      conn.Read(header)
      length := binary.BigEndian.Uint32(header)
    2. 动态分配内存:根据长度字段分配缓冲区。

      data := make([]byte, length)
      conn.Read(data)
    3. 预分配缓冲区:对于固定长度的消息,预先分配固定大小的缓冲区。

      buffer := make([]byte, 1024) // 假设最大消息长度为1024
      n, _ := conn.Read(buffer)
  • 注意事项

    • 避免缓冲区溢出:限制最大消息长度(如MAX_PACKET_SIZE)。
    • 处理分片:如果数据包分多次到达,需缓存未接收部分。
    • 校验数据完整性:通过校验和或CRC验证数据是否完整。

欢迎关注 ❤

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

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

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


王中阳讲编程
836 声望324 粉丝