跟复旦硕士聊了1小时,没想到这些基础题他居然也栽了

大家好,我是地鼠哥,我是今天发文的小编。后面也会在阳哥的号分享更多干货经验,欢迎大家关注我们。

今天要跟大伙分享的,是我和组织内一位学员的 Go 语言模拟面试内容。

先介绍下他的基本情况:最高学历是复旦硕士 ,学的软件工程,有 2 年工作经验(非Go岗位),之前薪资 22k,目标是 3 个月内成功上岸。他的学历是真的很不错,只是可惜前面几年走了弯路(后悔自己当年没有进互联网公司),现在想及时止损,冲击一波大厂。

主要挑几个他回答得不太理想的问题好好说一下 ,看文章的朋友也可以琢磨琢磨:这些问题换作你来,能答到点子上吗?

下面咱们就结合对话记录,一个个聊这些问题:

问题 1:子协程的 panic 能被父协程捕获吗?

我:父协程能捕获到子协程的 panic 吗?

他:可以的。

我:你有去试过吗?

他:这个我没有去试过。我没试过,但是我好像有看到。

问题背景:协程是 Go 并发的核心,而 panic 处理直接关系到程序稳定性。这个问题考的就是对协程间错误传递机制的实际理解。

他回答里的问题:上来就说 "可以的",但紧接着又承认 "没试过"。这暴露了两个点:一是对知识点记混了,二是缺了 动手验证的意识。很多朋友学 Go 时总靠死记硬背,觉得 理论懂了就行,但实际上写几行代码跑一跑,比记十遍结论都管用。

正确的回答该怎么说: 在 Go 里,子协程的 panic 没法被父协程直接捕获。

  • 协程是独立的执行单元,它的 panic 只会终止自己;要是没在子协程内部用defer+recover处理,整个程序都会崩。
  • 真想在协程间传递错误,得靠 channel 主动传(比如chan error),或者用errgroup这类工具包管理。

举个简单例子:

go 复制代码
func main() {
    errCh := make(chan error)
    go func() {
        defer func() {
            if e := recover(); e != nil {
                errCh <- fmt.Errorf("子协程出错:%v", e)
            }
        }()
        // 子协程里故意触发panic
        panic("出错了")
    }()
    // 父协程通过channel接错误
    if err := <-errCh; err != nil {
        fmt.Println("抓到子协程错误:", err)
    }
}

问题 2:怎么保证 map 遍历是有序的?

我:map 是无序的,我们要怎么样保证它的有序性?

他:在一些 C++ 这类语言中,会把它放到 set 集合里。

我:在 Go 语言里没有 set 这个数据结构,现在问的是 Go 相关的。

他:那在 Go 里怎么保证 map 有序性?我想是不是可以新建一个堆,把数据存进去,按大小对应这样。

问题背景:Go 的 map 遍历顺序是随机的,但实际开发中经常需要按固定顺序(比如插入顺序、key 的字典序)遍历,这题考的是 "结合 Go 特性解决实际问题" 的能力。

他回答里的问题:先是说到 C++ 的 set,明显没聚焦 Go;后来又说 "用堆",把简单问题复杂化了。这说明对 Go 里数据结构的实际用法不熟悉,没养成 "用 Go 的方式解决 Go 问题" 的思维。

正确的回答该怎么说: Go 里保证 map 有序遍历,核心思路就是 "用切片记顺序",两种常见办法:

  1. 按插入顺序遍历:插 map 的时候,同步把 key 放进切片,遍历切片时按顺序取 map 的 value;
  2. 按 key 排序后遍历:先把 map 的 key 全放到切片里,排序后再遍历访问 map。

举个插入顺序的例子:

go 复制代码
func main() {
    m := make(map[string]int)
    keys := []string{} // 用切片记插入的key
    // 插数据
    m["x"] = 10
    keys = append(keys, "x")
    m["y"] = 20
    keys = append(keys, "y")
    // 按插入顺序遍历
    for _, k := range keys {
        fmt.Printf("%s: %d\n", k, m[k]) // 输出x:10 y:20
    }
}

问题 3:select 语句的实现原理是什么?

我:select 实现原理你之前有了解过吗?

他:select 的实现原理我没有了解过,我觉得它可能和 IO 多路复用可能是有类似的机制。比如在 C++ 里,是基于内核,内部实现了一个队列,当队列快塞满时,操作系统会发信号,用户接收后从队列里取监听的字段,然后遍历处理。

我:你说的是 C++ 里的,建议后面把 Go 相关的也了解下,可以对比下区别。

问题背景:select 是 Go 实现 channel 多路复用的关键,懂它的原理才能真正用好并发。

他回答里的问题:直接说 "没了解过 Go 的实现",还是用C++ 的机制来套(看来他C++学的挺不错哈哈哈)。这就反映出对 Go 核心语法的底层逻辑理解不到位 ------ 很多朋友会用 select,但不知道它为啥能高效管理多个 channel,遇到复杂场景就容易掉坑。

正确的回答该怎么说: Go 的 select 底层是 "伪随机 + 轮询" 机制,大概分两步:

  1. 编译时:把每个 case 转成scase结构体,存着 channel 指针、操作类型(发 / 收)、数据指针这些;
  2. 运行时:
    • 先查所有 case 里的 channel 有没有就绪的(比如能发 / 能收),有的话随机挑一个执行(避免某个 case 饿死);
    • 要是都没就绪,有 default 就走 default;
    • 没 default 的话,当前协程就阻塞,等任意 channel 就绪了再唤醒。

说白了,select 就是为了高效处理多个 channel 的并发操作,不让单个 channel 阻塞拖慢整个程序。

问题 4:在高并发和内存密集型场景下,GC 的触发时机、调优策略以及对程序性能的影响

我:在高并发和内存密集型场景下,GC 的触发时机、调优策略以及对程序性能的影响你了解过吗?

他:GC 触发时机我只知道有定时触发,其他的不太清楚。

问题背景:GC 是 Go 内存管理的核心,在高并发和内存密集型场景中,知道触发时机、调优策略以及对程序性能的影响才能优化程序性能。

他回答里的问题:只了解 GC 的定时触发时机,对在高并发和内存密集型场景下的调优策略以及 GC 对程序性能的影响缺少了解。这说明对 GC 机制在复杂场景下的应用和优化理解不够,在实际开发过程中如果遇到了类似问题就无法应对了。

正确的回答该怎么说: Go 的 GC 触发主要有三种情况:

  1. 定时触发:默认超过 2 分钟没 GC,就强制来一次(防止内存一直不回收);
  2. 内存阈值触发 :新分配的内存占已用内存的比例超过阈值(由GOGC控制,默认 100,也就是新分配的和已用的一样多时触发);
  3. 主动触发 :用runtime.GC()手动调(比如程序退出前清理资源)。

调优策略

  • 调整 GOGC :在内存密集型场景下,可以适当增大GOGC的值,减少 GC 的频率,但可能会增加内存占用;反之,减小GOGC的值会增加 GC 的频率,但可以降低内存占用。
  • 对象池复用:使用对象池来复用对象,减少内存分配和回收的开销。
  • 分代回收思想:对于频繁创建和销毁的小对象,可以采用分代回收的思想,将其隔离处理。

对程序性能的影响

  • STW(Stop The World):GC 过程中会有短暂的 STW 阶段,此时所有的协程都会暂停,对高并发程序的性能影响较大。
  • 内存占用:不合理的 GC 配置可能会导致内存占用过高或频繁的内存分配和回收,影响程序的性能。

问题 5:new 和 make 的区别是什么?

我:那new 关键字和 make 这两个关键字的区别讲一下。 他:new 关键字,它是相当于是去分配一块地址,然后指向需要申请的元素,然后它返回的是一个指针。然后make的话它实际上返回的是一个引用类型,但是只是用在一些固定的这种数据结构,比如说是切片,然后 map 然后 channel。 我:那 new 能用到哪些结构上面? 他:比如说是一些基本的类型,比如说 int,然后那个或者自定义的一些结构体之类的都可以。 我:没错,其实基本上就都可以知道吧,然后其实你去new个切片 map channel 也行,只不过你new出来之后,它不能直接用,你还是要再去 make 一下才能去用。

问题背景:new 和 make 是 Go 中用于内存分配的两个核心关键字,面试中高频出现。

他回答里的问题:明显误区:认为 new "只用于基本类型和结构体",忽略了 new 其实可以用于所有类型(包括切片、map 等),只是用 new 创建这些类型后无法直接使用,必须再初始化。这说明对他们的适用场景和底层作用理解不够 透彻。

正确的回答该怎么说 : new 和 make 的核心区别体现在适用类型、返回值、作用三个方面:

  1. 适用类型
    • new:可用于所有类型(基本类型、结构体、切片、map 等);
    • make:仅用于切片(slice)、映射(map)、通道(channel)这三种引用类型。
  2. 返回值
    • new:返回指向类型的指针(如*int*[]int),分配的内存会被初始化为 "零值"(如 int 的 0、string 的 "");
    • make:返回类型本身(如[]intmap[string]int),分配的内存会被 "初始化"(如切片会创建底层数组并设置长度和容量,map 会初始化桶结构)。
  3. 作用
    • new:仅负责 "分配内存",不处理类型特有的初始化逻辑(比如用 new 创建的切片,底层数组未被真正初始化,无法直接 append 元素);
    • make:不仅分配内存,还会执行类型特有的初始化(比如创建切片时指定容量,创建 map 时初始化哈希表,创建 channel 时设置缓冲区)。

举个例子对比:

go 复制代码
// new的用法:返回指针,需手动初始化
func main() {
    // new创建int,返回*int,值为0(零值)
    num := new(int)
    fmt.Println(*num) // 输出0

    // new创建切片,返回*[]int,但切片未初始化,无法直接使用
    s := new([]int)
    // *s = append(*s, 1) // 需先初始化(如分配底层数组)才能使用
}

// make的用法:返回类型本身,可直接使用
func main() {
    // make创建切片,返回[]int,已初始化(长度0,容量10)
    s := make([]int, 0, 10)
    s = append(s, 1) // 可直接使用

    // make创建map,返回map[string]int,已初始化哈希表
    m := make(map[string]int)
    m["a"] = 1 // 可直接赋值
}

简单说:new 是 "通用内存分配工具",只负责给变量找块地方放;make 是 "专用初始化工具",专为三种引用类型做开箱即用的准备。

欢迎关注 ❤

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

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

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

相关推荐
寅时码12 分钟前
无需安装,纯浏览器实现跨平台文件、文本传输,支持断点续传、二维码、房间码加入、粘贴传输、拖拽传输、多文件传输
前端·后端·架构
大葱白菜1 小时前
Maven 入门:Java 开发工程师的项目构建利器
java·后端·程序员
大葱白菜1 小时前
Maven 与单元测试:JavaWeb 项目质量保障的基石
java·后端·程序员
二闹1 小时前
一行配置搞定微服务鉴权?别急,真相在这里!
后端·spring cloud·微服务
will_we1 小时前
服务器主动推送之SSE (Server-Sent Events)探讨
前端·后端
天道佩恩1 小时前
WebFlux响应式编程基础工程搭建
java·后端·响应式编程
葫芦和十三1 小时前
解构 Coze Studio:DDD 与整洁架构的 Go 语言最佳实践
后端·领域驱动设计·coze
黑暗也有阳光1 小时前
java 集合中arrayList为什么查询比较快,而插入和删除比较慢
java·后端·面试
Echo451 小时前
Linux的命令和Docker的命令记录
后端
Code季风1 小时前
什么是微服务分布式配置中心?
分布式·微服务·go