求职 记一次失败到连面试题都没过去的 Go 语言面试

laeo · 2020年09月27日 · 最后由 astaxie 回复于 2020年09月27日 · 251 次阅读

断断续续学习、关注 Go 语言相关技术也有段时间了(大概一两年吧),今年是靠着疫情后的大段休息时间,狠下心决定要找找 Go 语言方面的岗位,简历投出去却是如石沉大海,不是已查阅,就是不合适。考虑到自身学历和工作经历,也觉得有这般结果是正常的,但仍是不甘心如此结束,也就偶尔看到合适的岗位就尝试投一投。功夫不负有心人,总算是有一家公司的人事联系让我去面试,这篇文章就是记录我在面试前、面试中、面试后的各种准备、思考。

准备时

第一次参加 Go 语言岗位的面试,也不知道会问些什么问题,根据自身情况,我投的都是偏业务逻辑开发的岗位,所以猜测最多也就是常规的语言知识,后端技术栈相关的东西吧。于是我就主要搜索了下 Go 语言相关的面试题,优先看语言方面的题目,对于数据库、缓存之类的题就没去看。坐一路的公交,就低头看了一路的面试题,着重看了其中与语言特性有关的各种题目,比如结构的值接收者方法与指针接收者方法的差异、chan 与协程的搭配使用、defer 关键词等等,都是平时在 IDE 的帮助下,处理过,但未曾关注过的信息。

面试题

拿到面试题的一瞬间,我就开始懵逼了,有一部分 “大庭广众之下” 的紧张感(或许是担心作弊,该公司并没有让我在会议室做题,而是应该在两个部门办公桌之间过道的桌子上做题),也有一部分第一次参加此语言岗位的紧张感,整个人都不好了,于是大脑一片空白。

看题目,

协程,线程,进程的区别。

  • 我的答案:

    协程是用户态的,由软件实现。线程分内核态和用户态,存在于进程中。线程是 CPU 执行的最小单元。

  • 相关资料:

  • 点评:

    进程是分配资源的最小单位,线程是 CPU 调度的最小单位,协程是工程师或语言自建的调度单位。一步步看过来,就是在层层拆分调度时的 “块”,提升调度的精度。

    最开始以进程来调度,发现成本太高,然后将进程的计算逻辑拆分成多个小块,根据情况进行调度,结果发现在大规模系统下资源耗费还是很高,于是继续拆为更小的计算块,在线程内部再次进行调度。而最高层级的进程,反而成为了只存储数据的模型层,线程倒是变成了控制器层,协程成了控制器中完成逻辑所调用到的各种方法。不知道这样理解是否正确,但确实是我看了这几份资料后的感觉。

无缓冲 Chan 的发送和接收是否同步?

Golang 中是否需要重入锁?

  • 我的答案:

    不清楚此概念。

  • 相关资料:

  • 点评:

    自是不必!

    最开始看到这个重入锁我就有点奇怪,从来没在任何所学语言的文档、注释、教程中看到过这个概念,回家的路上一搜,果不其然——JAVA……想来也对,只有这门语言毛病最多。地铁上把第一篇文章看完了,对这概念有了个了解,下意识就觉得这设计有点反人类,又想到 Go 中肯定不需要用到,如果有,那肯定是代码写的有问题。

    从入口函数开始,执行的层次都是一层层往下的,如果有一个锁需要共享给几个函数,那在调用这几个函数的上层,直接加好锁不就可以了吗?不必搞什么新概念,每个函数中都加一次锁,又要每个函数都去释放,为了解决一个问题,又引入新的问题,实不可取。看了 SF 帖子里的大佬的邮件内容,安心了。

一个 uint 类型的值 a=1,和一个 uint 类型的值 b=2,a - b 结果是多少

  • 我的答案:

    应该是 1。

  • 实践代码:

    func main() {
        var a uint = 1
        var b uint = 2
        fmt.Printf("%d", a-b) // 18446744073709551615
    }
    
  • 相关资料:

  • 点评:

    不禁感叹野路子出生的猿好难。这应该是一个科班出身的程序员的常识了,甚至我在某群里发出这个题目的时候,还有大佬不假思索就给出了答案。这个结果是根据计算机架构来决定的,因为 Go 语言中的 int、uint 类型并非 int32、uint32 的别名,而是一个独立类型。在 32 位机器中 int 的长度与 int32 相同,而在 64 位的机器中是与 int64 相同,所以在我电脑上打印出来的这个值,与 math.MaxUnt64 相等。

    至于为什么一个 uint(-1) 会打印出这个最大值,看了这三篇资料后,在 SF 帖子中找到了简单的一句话,

    Since the value is negative, UINT_MAX + 1 is added to it so that the value is a valid unsigned quantity.

    带入上面的代码中就能计算出答案,math.MaxUint64 + 1 - 1 就是为什么会得出这个结果,不难想象,如果是 32 位计算机上,结果会是 math.MaxUint32 + 1 - 1。另外我也做了尝试将 b 的值改为 3,结果确实也是 math.MaxUint64 + 1 - 2。至于具体的运算原理,看了一遍补码的百科后,我已经绕晕在 0 和 1 的海洋,需要多看看才能顿悟。

如何解决 Golang 中的循环依赖?

  • 我的答案:

    将循环处单独抽离。

  • 点评:

    这个问题我以前遇到过,比如包 A 引用了 B,然后 B 又引用到了 A,造成了循环引用,编译时会报错。这个问题解决起来也没别的办法,只能对代码结构进行整理,将互相的依赖抽离,单独放到第三个包中,不管是直接引用还是通过接口间接引用。

方法和函数有什么区别?

  • 我的答案:

    方法有一个隐藏参数,函数没有。

  • 点评:

    个人感觉回答的方向是对的,Go 语言中没有类的概念,实际上也就不存在 “类方法”,函数是独立的一个代码块,方法则与类直接关联。Go 语言中常说的方法,其实应该是 接收者函数 Receiver Function,它会比常规的函数多一个隐藏参数,通过在 func 关键词和函数名之间声明,程序在执行到该函数时,会自动将这个参数传递给函数。

下面代码是否有问题,如有则说明

package main

import "fmt"

type People struct {
    Name string
}

func (p People) String() string {
    return fmt.Sprintf("print: %v", p)
}

func main() {
    p := &People{}
    p.String()
}
  • 我的答案:

    fmt 会调用 Stringer 接口方法,由此产生死循环。

  • 点评:

    代码放到 VSCode 里,立马就提示我 Sprintf format %v with arg p causes recursive String method call,然后又 result of (*interview.People).String call not used,除此之外并无别的错误。那么正常来讲,我的答案虽不完整(还有函数调用未使用的问题),但也不至于不对吧?当时面试官说我答得不好,我还指着这道题问他说,至少这道是对的吧,他告诉我说不对……也不知是敷衍,还是确实有什么问题我没看出来……

请解释 CAP 的原理以及是如何保证线程安全的

  • 我的答案:

    只看过 GPM 的资料,CAP 的还未知,会补上。

  • 相关资料:

    未搜到相关资料,懵逼……

给定一个字符串 s,请计算 s 是否由相同字符串重复多次(至少两次)得到。

例如:s=ababab,计算返回:true。原因:由 ab 重复三次得到。

s=abcab,计算返回:false。原因 s 并不能由一个字符串重复多次得到。

请使用 go 写出入参是 s 返回值为 bool 类型的方法

  • 我的答案:

    这是要求纸上手写代码,说实话体验非常不好,故在此用代码将我的思路实现。

    func repeatedSubstr(s string) bool {
        if s == "" {
            return false
        }
        if len(s) < 2 {
            return true
        }
        for i := 1; i < len(s); i++ {
            may := len(s) / i
            if may > 1 && s == strings.Repeat(s[0:i], may) {
                return true
            }
        }
    
        return false
    }
    
  • 相关资料:

  • 点评:

    毫无疑问,我当时能想到的就是暴力解法,如果由重复的子字符串组成,那么依次取一个片段,重复一定次数,一定能有与原字符串相等的时候,否则该字符串就不是由重复的子字符串组成。看过资料后才想起来,其实也不必每次都用片段去构建完整字符串,只需在循环内部再套一个循环,利用字符串索引遍历匹配每个片段即可。当然最佳答案肯定是用 KMP 算法来处理,可惜我当时不会这,平时也疏于对算法的学习、练习。

    力扣评论区有些奇怪的算法,重复一次母字符串,然后掐头去尾,再判断母字符串是否存在于其中,如果字符串由重复子串构成,那么就算掐头去尾后的双份母串中,也一定能找出与原母串相同的字符串。比如 abab 作为母串,重复后为 abababab,掐头去尾后为 bababa,那么里面是不是有原始字符串 abab?就是这么个逻辑。但是这里有个问题,就是判断原字符串是否存在于处理后的字符串中,这个逻辑也挺复杂,遇到较真的,非要你写出来……

总结和感想

说实话这次面试是关注、学习、使用 Go 语言这么多年来,第一次正式的参加面试,为此我还一路上都在看面试题,以期望能有个好结果。万万没想到,我连会议室的门都没进去,就灰溜溜离开了。属实难受,一度想放弃 Go,继续安心 PHP 或者 JS 了。

但是到家后仔细想了想,也感觉有这结果并不出意料,Go 语言的求职环境已经不是两三年前那样了,当初招人都是 PHP 转 Go,弱类型的脚本语言毕竟都是追求开发速度的,可现在市场逐渐饱和,企业招人也开始精益求精,要求基础要求底子。越往后,越难搞。

这次失败的面试,算是给我提了个醒,K8S、容器、分布式、微服务之类,虽然也是重要的,可是要是连基础都不行,怕是连会议室都进不去,更别提面试。如果一直用学 PHP 的心态学 Go,迟早还得滚回去继续写 PHP。

GOCN 特别备注:第一次将面试的过程记录下来,并发布到社区,挺忐忑的,希望大佬们轻喷,我心灵比较脆弱 :( 对于文中有差错的地方,烦请斧正,我会立刻修改。🙏

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio

社区的马克铛解析是不是有特别的设置啊?我原文在 VSCode 下开着预览写的,放到博客上也正常,但是原样复制到社区后,预览结果就有很大差异,比如列表之类的排版都乱了。

感觉社区的 markdown 有点问题

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册