分享
  1. 首页
  2. 文章

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

wangzhongyang007 · · 251 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

大家好,我是地鼠哥,我是今天发文的小编。后面也会在阳哥的号分享更多干货经验,欢迎大家关注我们。 今天要跟大伙分享的,是我和组织内一位学员的 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:返回类型本身(如`[]int`、`map[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**,备注:面试群。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
251 次点击 ∙ 1 赞
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏