
大家好,我是地鼠哥,我是今天公众号的小编。后面也会在阳哥的公众号分享更多干货经验,欢迎大家关注我们。
今天要跟大伙分享的,是我和组织内一位学员的 Go 语言模拟面试内容。
先介绍下他的基本情况:最高学历是复旦硕士,学的软件工程,有 2 年工作经验(非Go岗位),之前薪资 22k,目标是 3 个月内成功上岸。他的学历是真的很不错,只是可惜前面几年走了弯路(后悔自己当年没有进互联网公司),现在想及时止损,冲击一波大厂。
主要挑几个他回答得不太理想的问题好好说一下 ,看文章的朋友也可以琢磨琢磨:这些问题换作你来,能答到点子上吗?
下面咱们就结合对话记录,一个个聊这些问题:
我:父协程能捕获到子协程的 panic 吗? 他:可以的。 我:你有去试过吗? 他:这个我没有去试过。我没试过,但是我好像有看到。
问题背景:协程是 Go 并发的核心,而 panic 处理直接关系到程序稳定性。这个问题考的就是对协程间错误传递机制的实际理解。
他回答里的问题:上来就说 “可以的”,但紧接着又承认 “没试过”。这暴露了两个点:一是对知识点记混了,二是缺了 动手验证的意识。很多朋友学 Go 时总靠死记硬背,觉得 理论懂了就行,但实际上写几行代码跑一跑,比记十遍结论都管用。
正确的回答该怎么说: 在 Go 里,子协程的 panic 没法被父协程直接捕获。
defer+recover处理,整个程序都会崩。chan error),或者用errgroup这类工具包管理。举个简单例子:
func main() {
errCh := make(chan error)
gofunc() {
deferfunc() {
if e := recover(); e != nil {
errCh <- fmt.Errorf("子协程出错:%v", e)
}
}()
// 子协程里故意触发panic
panic("出错了")
}()
// 父协程通过channel接错误
if err := <-errCh; err != nil {
fmt.Println("抓到子协程错误:", err)
}
}
我:map 是无序的,我们要怎么样保证它的有序性? 他:在一些 C++ 这类语言中,会把它放到 set 集合里。 我:在 Go 语言里没有 set 这个数据结构,现在问的是 Go 相关的。 他:那在 Go 里怎么保证 map 有序性?我想是不是可以新建一个堆,把数据存进去,按大小对应这样。
问题背景:Go 的 map 遍历顺序是随机的,但实际开发中经常需要按固定顺序(比如插入顺序、key 的字典序)遍历,这题考的是 “结合 Go 特性解决实际问题” 的能力。
他回答里的问题:先是说到 C++ 的 set,明显没聚焦 Go;后来又说 “用堆”,把简单问题复杂化了。这说明对 Go 里数据结构的实际用法不熟悉,没养成 “用 Go 的方式解决 Go 问题” 的思维。
正确的回答该怎么说: Go 里保证 map 有序遍历,核心思路就是 “用切片记顺序”,两种常见办法:
举个插入顺序的例子:
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
}
}
我:select 实现原理你之前有了解过吗? 他:select 的实现原理我没有了解过,我觉得它可能和 IO 多路复用可能是有类似的机制。比如在 C++ 里,是基于内核,内部实现了一个队列,当队列快塞满时,操作系统会发信号,用户接收后从队列里取监听的字段,然后遍历处理。 我:你说的是 C++ 里的,建议后面把 Go 相关的也了解下,可以对比下区别。
问题背景:select 是 Go 实现 channel 多路复用的关键,懂它的原理才能真正用好并发。
他回答里的问题:直接说 “没了解过 Go 的实现”,还是用C++ 的机制来套(看来他C++学的挺不错哈哈哈)。这就反映出对 Go 核心语法的底层逻辑理解不到位 —— 很多朋友会用 select,但不知道它为啥能高效管理多个 channel,遇到复杂场景就容易掉坑。
正确的回答该怎么说: Go 的 select 底层是 “伪随机 + 轮询” 机制,大概分两步:
scase结构体,存着 channel 指针、操作类型(发 / 收)、数据指针这些;说白了,select 就是为了高效处理多个 channel 的并发操作,不让单个 channel 阻塞拖慢整个程序。
我:在高并发和内存密集型场景下,GC 的触发时机、调优策略以及对程序性能的影响你了解过吗? 他:GC 触发时机我只知道有定时触发,其他的不太清楚。
问题背景:GC 是 Go 内存管理的核心,在高并发和内存密集型场景中,知道触发时机、调优策略以及对程序性能的影响才能优化程序性能。
他回答里的问题:只了解 GC 的定时触发时机,对在高并发和内存密集型场景下的调优策略以及 GC 对程序性能的影响缺少了解。这说明对 GC 机制在复杂场景下的应用和优化理解不够,在实际开发过程中如果遇到了类似问题就无法应对了。
正确的回答该怎么说: Go 的 GC 触发主要有三种情况:
GOGC控制,默认 100,也就是新分配的和已用的一样多时触发);runtime.GC()手动调(比如程序退出前清理资源)。GOGC的值,减少 GC 的频率,但可能会增加内存占用;反之,减小GOGC的值会增加 GC 的频率,但可以降低内存占用。我:那new 关键字和 make 这两个关键字的区别讲一下。 他:new 关键字,它是相当于是去分配一块地址,然后指向需要申请的元素,然后它返回的是一个指针。然后make的话它实际上返回的是一个引用类型,但是只是用在一些固定的这种数据结构,比如说是切片,然后 map 然后 channel。 我:那 new 能用到哪些结构上面? 他:比如说是一些基本的类型,比如说 int,然后那个或者自定义的一些结构体之类的都可以。 我:没错,其实基本上就都可以知道吧,然后其实你去new个切片 map channel 也行,只不过你new出来之后,它不能直接用,你还是要再去 make 一下才能去用。
问题背景:new 和 make 是 Go 中用于内存分配的两个核心关键字,面试中高频出现。
他回答里的问题:明显误区:认为 new “只用于基本类型和结构体”,忽略了 new 其实可以用于所有类型(包括切片、map 等),只是用 new 创建这些类型后无法直接使用,必须再初始化。这说明对他们的适用场景和底层作用理解不够 透彻。
正确的回答该怎么说: new 和 make 的核心区别体现在适用类型、返回值、作用三个方面:
*int、*[]int),分配的内存会被初始化为 “零值”(如 int 的 0、string 的 "");[]int、map[string]int),分配的内存会被 “初始化”(如切片会创建底层数组并设置长度和容量,map 会初始化桶结构)。举个例子对比:
// 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 是 “专用初始化工具”,专为三种引用类型做开箱即用的准备。