寫(xiě)給想去字節(jié)寫(xiě) Go 的你
寫(xiě)作本文的原因是回答知乎提問(wèn):為什么字節(jié)跳動(dòng)選擇使用 Go 語(yǔ)言?
先做下自我介紹,目前人在字節(jié)做后端開(kāi)發(fā),工作語(yǔ)言主要是 Go,字節(jié)萬(wàn)千面試官之一,出版圖書(shū)《C++ 服務(wù)器開(kāi)發(fā)精髓》一書(shū)。在來(lái)字節(jié)使用 Go 之前,我寫(xiě)了多年 C/C++ 和 Java。
為什么字節(jié)跳動(dòng)選擇使用 Go 語(yǔ)言?
確實(shí)如題主所說(shuō),字節(jié)后端服務(wù)大多使用 Go。那為什么字節(jié)跳動(dòng)選擇使用 Go 語(yǔ)言呢?
目前后端服務(wù)能勝任大型項(xiàng)目的編程語(yǔ)言有 C/C++、Java 和 Go,在給出答案之前,我們先看幾個(gè)編程語(yǔ)言之間對(duì)比的例子。
例子一
有下面一段 Go 代碼:
- // Go代碼
- func CreateItem(id int, name string) *Item {
- myItem := Item{ID: id, Name: name}
- return &myItem
- }
C/C++ 轉(zhuǎn) Go 語(yǔ)言的同學(xué)剛開(kāi)始看到這樣的代碼可以正常運(yùn)行是不敢相信的,在早些年的計(jì)算機(jī)的編程語(yǔ)言入門(mén)課中,老師和無(wú)數(shù)課本一直告誡我們:不要返回一個(gè)局部變量(棧變量)的地址,因?yàn)樵诤瘮?shù)調(diào)用結(jié)束后,棧被銷毀,引用已經(jīng)銷毀的棧中的變量可能會(huì)出現(xiàn)內(nèi)存問(wèn)題。然而,這樣的代碼在 Go 中工作的很好,也很常用。
然而 Java 開(kāi)發(fā)的同學(xué)就很習(xí)慣這樣的代碼,因?yàn)樵?Java 中隨處可見(jiàn)這樣的代碼,但是,Java 中創(chuàng)建 myItem 這樣的對(duì)象畢竟使用的是堆內(nèi)存呀。如果熟悉 Java 虛擬機(jī)回收內(nèi)存的常見(jiàn)方法,對(duì) Go 語(yǔ)言中這種用法也會(huì)了然于心。Go 開(kāi)發(fā)者之所以可以這樣寫(xiě)代碼,是因?yàn)?Go 編譯器替我們做了額外的內(nèi)存分配和回收工作。從這個(gè)意義上來(lái)講,C++ 也可以使用堆內(nèi)存實(shí)現(xiàn)同樣的功能,代碼如下:
- // C++代碼
- Item* CreateItem(int id, string name) {
- Item* myItem = new Item(id, name);
- return myItem;
- }
在 C++ 中需要開(kāi)發(fā)者自己記得在必要的時(shí)候釋放 myItem 占用的內(nèi)存,當(dāng)然,從 C++ 的角度來(lái)說(shuō),這樣的做法是一種不好的實(shí)踐:C++ 最佳實(shí)踐建議內(nèi)存是誰(shuí)分配的就由誰(shuí)來(lái)釋放。在這個(gè)函數(shù)中分配內(nèi)存,在另外一個(gè)函數(shù)中釋放內(nèi)存,一般不推薦這么做。
例子二
前段時(shí)間有位同學(xué)來(lái)面試,我看這位同學(xué)簡(jiǎn)歷上寫(xiě)了熟悉 C++ 和 Go,我就問(wèn)這位同學(xué)覺(jué)得 Go 語(yǔ)言中有哪些好用的特性,該同學(xué)提到了 defer 關(guān)鍵字,于是我們就 Go 中的 defer 關(guān)鍵字討論了一下,我的問(wèn)題是:用 C++ 可以實(shí)現(xiàn) defer 關(guān)鍵字的等價(jià)功能嗎?
以下是該同學(xué)的回答:
結(jié)論是可以的,因?yàn)?Go 中 defer 關(guān)鍵字是在當(dāng)前函數(shù)執(zhí)行完畢(或者說(shuō)是出了當(dāng)前函數(shù)作用域)時(shí),自動(dòng)執(zhí)行一些我們指定的動(dòng)作,一般用于資源的回收。代碼如下:
- //Go代碼
- func ReadFile(fileName string) {
- myFile, err := os.Open(fileName)
- if err != nil {
- return
- }
- //讀取文件的過(guò)程中,無(wú)論哪一步出錯(cuò),最終文件均會(huì)被關(guān)閉
- defer myFile.Close()
- myData1 := make([]byte, 5)
- _, err = myFile.Read(myData1)
- if err != nil {
- return
- }
- fmt.Println(string(myData1))
- myData2 := make([]byte, 5)
- _, err = myFile.Read(myData2)
- if err != nil {
- return
- }
- fmt.Println(string(myData2))
- }
上述 Go 代碼中,由于使用了 defer 語(yǔ)句,無(wú)論哪一步的 myFile.Read 函數(shù)操作失敗了,最終文件都會(huì)被關(guān)閉,避免了文件句柄的泄漏。
在 C++ 中我們可以使用 RAII 技術(shù)實(shí)現(xiàn)同樣的效果,只要構(gòu)造一個(gè) RAII 類即可,即在類的析構(gòu)函數(shù)關(guān)閉文件,這樣這個(gè)文件一旦打開(kāi),只要出了函數(shù)作用域都會(huì)被關(guān)閉。代碼如下:
- //C++代碼
- class File
- {
- public:
- File()
- {
- }
- ~File()
- {
- Close();
- }
- bool Open(const std::string& fileName)
- {
- m_myFile = fopen(fileName.c_str(), "r");
- return m_myFile != NULL;
- }
- int Read(char* buf, int length)
- {
- if (m_myFile == NULL)
- return -1;
- return fread(buf, 1, length, m_myFile);
- }
- void Close()
- {
- if (m_myFile != NULL)
- {
- fclose(m_myFile);
- }
- }
- private:
- FILE* m_myFile;
- };
- void ReadFile(const char* fileName)
- {
- //當(dāng)myFile出了作用域之后,會(huì)自動(dòng)調(diào)用File類的析構(gòu)函數(shù),在析構(gòu)函數(shù)中關(guān)閉文件句柄
- File myFile;
- bool success = myFile.Open(fileName);
- if (!success)
- return;
- char myData1[6] = {0};
- int count1 = myFile.Read(myData1, 5);
- if (count1 <= 0)
- return;
- std::cout << myData1 << std::endl;
- char myData2[6] = {0};
- int count2 = myFile.Read(myData2, 5);
- if (count2 <= 0)
- return;
- std::cout << myData2 << std::endl;
- }
上述 C++ 代碼中利用了 RAII 技術(shù)達(dá)到了和 Go defer 關(guān)鍵字一樣的效果,這位面試的同學(xué)表達(dá)的就是這個(gè)意思。
當(dāng)然,這位同學(xué)回答的并不完整,Go 的 defer 關(guān)鍵字所能達(dá)到的一些作用,C++ 中沒(méi)有與之等價(jià)的功能。
在 C++ 中,雖然在大多數(shù)情況下可以使用 RAII 技術(shù)達(dá)到在出了函數(shù)作用域時(shí)執(zhí)行我們指定的動(dòng)作,但是有一種情況 defer 關(guān)鍵字可以做到,C++ RAII 卻做不到,那就是當(dāng)函數(shù)執(zhí)行過(guò)程中有崩潰問(wèn)題(Go 中叫 panic)時(shí),可以在 defer 指定的動(dòng)作中恢復(fù)程序的執(zhí)行流,不讓整個(gè)進(jìn)程退出,而 C++ 程序是無(wú)法做到針對(duì)一個(gè)內(nèi)存問(wèn)題造成的 crash 恢復(fù)進(jìn)程繼續(xù)執(zhí)行的。Go 代碼如下:
- //Go代碼
- func RecoverFromCrash() {
- defer func() {
- //如果程序有崩潰,恢復(fù)程序
- recover()
- }()
- var pi *int
- *pi = 1
- }
- func main() {
- i := 1
- RecoverFromCrash()
- i = 2
- fmt.Println(i)
- }
上述 Go 代碼中,由于 RecoverFromCrash 函數(shù)操作了一個(gè)空指針,導(dǎo)致程序崩潰,然后 defer 配合 recover 函數(shù)可以恢復(fù)程序繼續(xù)運(yùn)行。也就是說(shuō),如果采用這種機(jī)制,一個(gè) goroutine 產(chǎn)生的崩潰不會(huì)影響到其他 goroutine。這在 C++ 中是絕對(duì)無(wú)法做到的,在 C++ 程序中,任何內(nèi)存問(wèn)題都會(huì)導(dǎo)致整個(gè)進(jìn)程退出。這外在表現(xiàn)就是,使用 Go 開(kāi)發(fā)出來(lái)的程序,一個(gè)接口有問(wèn)題不會(huì)影響到同一個(gè)服務(wù)中不相關(guān)的接口,更不會(huì)導(dǎo)致整個(gè)服務(wù)宕機(jī),這是 C++ 程序絕對(duì)無(wú)法做到的。
除此以外, defer 關(guān)鍵字還做了更多工作,defer 關(guān)鍵字在程序崩潰時(shí)可以恢復(fù)部分?jǐn)?shù)據(jù),這點(diǎn)很有用,我們可以基于此記錄一些程序崩潰時(shí)的現(xiàn)場(chǎng)值,這在 C++ 中根本做不到這一點(diǎn)。代碼如下:
- //Go代碼
- func ProcessRequest(req Request) (resp Response, log LoggerItem, err error) {
- defer func() {
- recover()
- }()
- resp = DoProcess1(req)
- resp = DoProcess2(req)
- //倘若程序在此處崩潰,我們可以在程序recover以后,仍然拿到前兩步處理后的resp值
- resp = DoProcess3(req)
- resp = DoProcess4(req)
- return
- }
C++ 開(kāi)發(fā)者無(wú)數(shù)次幻想過(guò)有一種方法可以在進(jìn)程內(nèi)恢復(fù)因內(nèi)存問(wèn)題而崩潰的進(jìn)程,更不用說(shuō)記錄進(jìn)程崩潰時(shí)的現(xiàn)場(chǎng)數(shù)據(jù)了,而這在 Go 中均做到了,而且如此簡(jiǎn)單易用。C++ 開(kāi)發(fā)者看到這里眼淚要流下來(lái)了。
作為面試官,該面試者能想到 C++ RAII 技術(shù)可以等價(jià)部分 Go 的 defer 關(guān)鍵字功能,我已經(jīng)很滿意了,倘若能更進(jìn)一步,就很完美了。
對(duì)于 Java 來(lái)說(shuō),我們可以使用 try -catch - finally 關(guān)鍵字實(shí)現(xiàn) defer 的上述功能,只要將代碼如下:
- //Java代碼
- try {
- } catch (SomeException e) {
- } finally {
- //Go中的defer需要做的工作放在這里
- }
雖然在 Java 中可以把 Go 中 defer 需要做的事情放到 finally 部分,但是實(shí)際業(yè)務(wù)中導(dǎo)致程序出現(xiàn)異常有很多原因,為了避免進(jìn)程不因?yàn)槲刺幚淼漠惓6顺?,我們必須捕獲頂級(jí) Exception,在 Java 中這是一種不推薦的方式。
好了,說(shuō)完這兩個(gè)例子之后,讓我們來(lái)對(duì)比一下 Go 與 C/C++/Java 的優(yōu)缺點(diǎn)。
性能與效率上的對(duì)比
C++ 最讓人詬病的問(wèn)題是需要開(kāi)發(fā)者自己管理內(nèi)存,從學(xué)習(xí)這塊知識(shí)的角度來(lái)看,編碼中直接管理內(nèi)存是管理不好的,開(kāi)發(fā)者必須學(xué)習(xí)與內(nèi)存相關(guān)的各種操作系統(tǒng)原理,最起碼要知道物理內(nèi)存與虛擬內(nèi)存、棧內(nèi)存與堆內(nèi)存、內(nèi)存分配與釋放時(shí)機(jī)、進(jìn)程地址空間的內(nèi)存分布、各個(gè)內(nèi)存地址區(qū)間的內(nèi)存讀寫(xiě)屬性、如何避免內(nèi)存越界等等相關(guān)知識(shí),而這些知識(shí)不是一蹴而就就能學(xué)會(huì)的,所以如果某個(gè)開(kāi)發(fā)者能寫(xiě)出一個(gè)經(jīng)年累月不需要重啟或者不宕機(jī)的 C++ 服務(wù),那他是個(gè)高手。但是在快速迭代業(yè)務(wù)的互聯(lián)網(wǎng)公司,怎么可能人人都是高手呢?萬(wàn)一某天來(lái)了個(gè)新人加了一個(gè)新功能,導(dǎo)致一個(gè)隱蔽的內(nèi)存問(wèn)題,之后就是惱人的問(wèn)題排查與定位。
C++ 自己管理內(nèi)存是把雙刃劍,高手可以用來(lái)寫(xiě)出高效的程序來(lái),但是對(duì)于新手或者水平不夠的開(kāi)發(fā)者來(lái)說(shuō),這將是企業(yè)產(chǎn)品事故甚至災(zāi)難的源泉。
再者,拜當(dāng)下各種焦慮的、淺嘗輒止的、急功近利的網(wǎng)文的宣傳,無(wú)論是個(gè)人還是公司,已經(jīng)沒(méi)有多少人愿意把時(shí)間花在學(xué)習(xí)周期長(zhǎng)、難度大的 C++ 語(yǔ)言之上了。只要在滿足業(yè)務(wù)要求的情況下,公司當(dāng)然愿意花更低的成本去使用更能保證業(yè)務(wù)快速、穩(wěn)定迭代的 Go 語(yǔ)言上了。
那么 Java 呢?Java 最大的問(wèn)題是,其編譯出來(lái)的程序不是操作系統(tǒng)原生支持的可執(zhí)行文件,必須運(yùn)行在 Java 虛擬機(jī)之中,這樣要想運(yùn)行必須依賴于 Java 虛擬機(jī),而對(duì)于復(fù)雜業(yè)務(wù)來(lái)說(shuō),生成的 Jar 文件也偏臃腫。所以無(wú)論是安裝 Java 程序的本身需要的運(yùn)行環(huán)境還是生成的 Jar 文件的執(zhí)行效率大大折扣。
我列一張表來(lái)對(duì)比一下 C++、Java 與 Go 在性能與可執(zhí)行文件體積上的差別,需要說(shuō)明一下,這張表是針對(duì)具有復(fù)雜功能的中大型項(xiàng)目來(lái)說(shuō)的:
執(zhí)行效率 | 可執(zhí)行文件體積 | 依賴運(yùn)行環(huán)境 | |
---|---|---|---|
C++ | 高 | 小 | 無(wú) |
Java | 低 | 大 | Java 虛擬機(jī) |
Go | 高 | 小 | 無(wú) |
語(yǔ)法層面上的對(duì)比
工作的早些年,我在使用 C/C++ 和 Java 進(jìn)行編程時(shí),曾思考這樣一個(gè)問(wèn)題,既然大多數(shù)的代碼行末尾必須都要以分號(hào)結(jié)束,那為啥編譯器不直接代勞此事?從編譯原理的角度來(lái)說(shuō),大多數(shù)代碼行末尾的分號(hào)都是沒(méi)有任何作用的。
而更早的學(xué)生時(shí)代,我常常因?yàn)橥浽谀承┐a行的結(jié)尾寫(xiě)上分號(hào)而導(dǎo)致代碼無(wú)法編譯通過(guò),我相信,在今天,數(shù)以萬(wàn)計(jì)的剛開(kāi)始接觸編程的同學(xué)也遇到和我曾經(jīng)一樣的問(wèn)題。
另外一個(gè)情形就是很多同學(xué)在寫(xiě) switch - case 語(yǔ)句的時(shí)候,有時(shí)候因?yàn)橥浽谔囟ǖ?case 語(yǔ)句之后寫(xiě)上 break 語(yǔ)句,從而導(dǎo)致程序執(zhí)行時(shí)出現(xiàn)非預(yù)期的行為,這個(gè)問(wèn)題也同樣困擾著學(xué)習(xí)編程的新人們。
一對(duì)大括號(hào)中的第一個(gè)大括號(hào)是否要單獨(dú)放在一行;if/for 等執(zhí)行體只有一條語(yǔ)句時(shí),是否應(yīng)該使用一對(duì)大括號(hào)包裹起來(lái),這類問(wèn)題在開(kāi)發(fā)者之間爭(zhēng)論了幾十年,并且將繼續(xù)在后來(lái)者那里爭(zhēng)論下去,就算是像《代碼大全》這樣經(jīng)典的書(shū)籍也花了好幾頁(yè)去討論這兩種代碼風(fēng)格哪種好,更不用說(shuō)各個(gè)公司為了統(tǒng)一編碼風(fēng)格而制定的各種代碼規(guī)范和 lint 檢查規(guī)則了。
- //到底哪種風(fēng)格好呢?
- //風(fēng)格1
- void DoTest() {
- }
- //風(fēng)格2
- void DoTest()
- {
- }
- //風(fēng)格1
- if (success) {
- printf("success");
- }
- //風(fēng)格2
- if (success)
- printf("success");
繼往開(kāi)來(lái),Go 語(yǔ)言大刀闊斧地去除了一些其他語(yǔ)言中看起來(lái)不是很必要的功能,這些功能的去除讓 Go 的風(fēng)格變得統(tǒng)一、簡(jiǎn)潔,在 Go 項(xiàng)目中,大家不會(huì)再為上文中提到的幾個(gè)風(fēng)格問(wèn)題而爭(zhēng)論了。
讓我們來(lái)看一下 Go 語(yǔ)言相對(duì)于其他語(yǔ)言所做的一些改動(dòng),歡迎讀者在評(píng)論區(qū)補(bǔ)充:
1. 每一行語(yǔ)句的結(jié)尾不再?gòu)?qiáng)行要求加上分號(hào)
- fmt.Println("hello world") //末尾不建議加;
2. 一對(duì)大括號(hào)的第一個(gè)不能單獨(dú)占一行
- //錯(cuò)誤的語(yǔ)法
- func DoTest()
- {
- }
- //正確的語(yǔ)法
- func DoTest() {
- }
3. if/for 等語(yǔ)句體只有一行時(shí)也必須使用一對(duì)大括號(hào)包裹起來(lái)
- //正確的語(yǔ)法
- if (success) {
- printf("success")
- }
- //錯(cuò)誤的語(yǔ)法
- if (success)
- printf("success")
4. if/for 等條件不再需要括號(hào)
- //正確的語(yǔ)法
- for i := 1; i < 10; i++ {
- fmt.Println(i)
- }
- //錯(cuò)誤的語(yǔ)法,for語(yǔ)句不需要括號(hào)
- for (i := 1; i < 10; i++) {
- fmt.Println(i)
- }
5. 只有 for 循環(huán),不再支持 while 和 do - while 循環(huán)
- //支持的語(yǔ)法
- for i := 1; i < 10; i++ {
- fmt.Println(i)
- }
- //不支持的語(yǔ)法
- while i < 100 {
- fmt.Println(i)
- i++
- }
6. switch - case 語(yǔ)句默認(rèn)加了 break 語(yǔ)句
- switch i {
- case 0:
- fmt.Println(0)
- case 1:
- fmt.Println(1)
- case 2:
- fmt.Println(2)
- default:
- }
- //相當(dāng)于
- switch i {
- case 0:
- fmt.Println(0)
- break
- case 1:
- fmt.Println(1)
- break
- case 2:
- fmt.Println(2)
- break
- default:
- }
如果你真的想執(zhí)行完一個(gè) case 接著執(zhí)行下一個(gè) case,只要使用 fallthrough 關(guān)鍵字就可以了:
- switch i {
- case 0:
- fmt.Println(0)
- fallthrough
- case 1:
- fmt.Println(1)
- case 2:
- fmt.Println(2)
- default:
- }
7. 自增自減運(yùn)算符只支持后綴形式,不支持前綴形式
- i := 0
- i++ //可以編譯通過(guò)
- ++i //無(wú)法通過(guò)編譯
8. 不支持條件運(yùn)算符(? :)
- b := 9
- a := (b > 0 ? true : false) //這一行無(wú)法通過(guò)編譯
9. 給一個(gè)結(jié)構(gòu)體多個(gè)字段設(shè)置值時(shí),最后一個(gè)字段也必須以逗號(hào)結(jié)束
- type StandardResp struct {
- Code int32
- Msg string
- Data interface{}
- }
- c.JSON(http.StatusOK, commonHttp.StandardResp{
- Code: 1000,
- Msg: "token error",
- Data: nil, //注意這里nil之后有一個(gè)逗號(hào),這在其他語(yǔ)法中必須沒(méi)有逗號(hào)
- })
以上列舉了 Go 精簡(jiǎn)后的一些語(yǔ)法要素,精簡(jiǎn)后的語(yǔ)法,讓編程初學(xué)者更容易記憶與上手。
極少的語(yǔ)法元素,讓 Go 簡(jiǎn)單易學(xué),字節(jié)的大多數(shù)同學(xué)都是入職后兩周內(nèi)學(xué)習(xí)的 Go,然后開(kāi)始著手業(yè)務(wù)開(kāi)發(fā)。
功能完備性的對(duì)比
Go 與 Java 相比較于 C++,其語(yǔ)言自帶的 API 庫(kù)功能是相當(dāng)完善的,從基本的字符串操作到網(wǎng)絡(luò)編程、文件讀寫(xiě)等等應(yīng)用盡有,因此 Go 的開(kāi)發(fā)者可使用的原生 API 就很豐富,比如編寫(xiě)一個(gè)網(wǎng)絡(luò)通信程序,Go 和 Java 都在 net 包中提供了大量可使用的 API,而 C++ 必須直接借助操作系統(tǒng)的 Socket API。
這就是我說(shuō)的語(yǔ)言的功能完備性,如果一個(gè)編程語(yǔ)言自帶的 API 越豐富,那么開(kāi)發(fā)者只要盡可能地掌握語(yǔ)言自身的 API 就可以了,自身的 API 通常會(huì)屏蔽了各個(gè)操作系統(tǒng)的差異性,學(xué)習(xí)成本更低,同樣是 Socket API,學(xué)習(xí) Go 或者 Java SDK 自帶的網(wǎng)絡(luò) API,要比直接學(xué)習(xí)多個(gè)操作系統(tǒng)的 Socket API 要容易得多。
對(duì)于 C++ 語(yǔ)言來(lái)說(shuō),隨著 C++ 標(biāo)準(zhǔn)的不斷發(fā)展,C++ 語(yǔ)言自身的功能完備性也在逐步完善,例如從 C++11 開(kāi)始,就可以直接使用 stl 中的線程相關(guān)的類,而不用再使用操作系統(tǒng)提供的線程接口。
結(jié)論
綜上所述,我給出我的結(jié)論,正因?yàn)?Go 語(yǔ)言簡(jiǎn)單易學(xué)、不容易出錯(cuò)、功能完備性良好且執(zhí)行效率高,特別適合字節(jié)這樣有超多超快的業(yè)務(wù)線產(chǎn)品迭代。當(dāng)然,Go 語(yǔ)言想入門(mén)容易,想學(xué)好成為高手并不容易,很多從其他語(yǔ)言轉(zhuǎn)到 Go 開(kāi)發(fā)的同學(xué),若不刻意勤加練習(xí),想寫(xiě)出地道、高效的 Go-Style 風(fēng)格的代碼也不是一件很容易的事情。
這里推薦幾本我學(xué)習(xí) Go 的書(shū)籍:
艾倫 《Go 程序設(shè)計(jì)語(yǔ)言》
許式偉 呂桂華《Go 語(yǔ)言編程》
雨痕 《Go 語(yǔ)言學(xué)習(xí)筆記》
求職字節(jié) Go 開(kāi)發(fā)崗位需要如何準(zhǔn)備?
相比較為什么字節(jié)跳動(dòng)選擇使用 Go 語(yǔ)言,很多同學(xué)可能更關(guān)心求職字節(jié)跳動(dòng) Go 開(kāi)發(fā)崗位要如何準(zhǔn)備。
這里有先消除一個(gè)錯(cuò)誤認(rèn)知:和大多數(shù) Go 崗位(字節(jié)的和非字節(jié)的)一樣,字節(jié)招 Go 開(kāi)發(fā)崗位的對(duì)求職者是否熟悉 Go 語(yǔ)言沒(méi)有強(qiáng)制要求,也就是說(shuō),你可以不熟悉 Go 語(yǔ)言也可以應(yīng)聘字節(jié)的 Go 開(kāi)發(fā)崗位,但是有幾個(gè)注意事項(xiàng):
一、雖然不要求熟悉 Go 開(kāi)發(fā),但要求熟悉至少一門(mén)編程語(yǔ)言
字節(jié)很多進(jìn)來(lái)做 Go 的同學(xué)之前都是做 C、C++ 或者 Java, 甚至是 php 或者 Python 的。所以,如果你之前根本沒(méi)接觸過(guò) Go 或者接觸過(guò)但不熟悉 Go,盡量不要在簡(jiǎn)歷中寫(xiě)自己熟悉 Go。這樣面試的時(shí)候面試官也就不會(huì)考察你任何關(guān)于 Go 本身的問(wèn)題,這點(diǎn)很重要,比如,你原來(lái)是做 C++ 或者 Java 的,你為了應(yīng)聘這個(gè)崗位強(qiáng)行寫(xiě)上自己熟悉 Go,那么面試官可能會(huì)重點(diǎn)考察一下你的 Go 技術(shù)棧,這樣你相當(dāng)于被考察一個(gè)不熟悉的技術(shù)棧,你很吃虧。我曾內(nèi)推的幾位同事,包括最近面試的一兩位同學(xué)都是這樣,拿自己弱項(xiàng)來(lái)接受考察,面試結(jié)果一般都不盡人意。以上文中那位同學(xué)為例,如果他不在簡(jiǎn)歷中寫(xiě)自己熟悉 C++ 和 Go,只寫(xiě)自己熟悉 C++,我就不會(huì)問(wèn)他任何關(guān)于 Go 的問(wèn)題了,比如 defer 關(guān)鍵字的問(wèn)題。
二、盡量寫(xiě)上一門(mén)自己擅長(zhǎng)的語(yǔ)言即可
這里的意思是,如果你之前是做 C++ 開(kāi)發(fā)的,那你就寫(xiě)上你熟悉 C++,Java 也一樣,這樣面試的時(shí)候,除了通用部分,面試官會(huì)考察你相應(yīng)的語(yǔ)言相關(guān)的內(nèi)容,例如簡(jiǎn)歷上寫(xiě)了熟悉 C++,如果連 C++ 虛函數(shù)的實(shí)現(xiàn)機(jī)制、stl 常用容器都說(shuō)不清楚,那明顯不符合預(yù)期;寫(xiě)熟悉 Java 的,HashMap、Java 的線程池 ThreadPoolExecutor、Java 虛擬機(jī)等都是常考的內(nèi)容。我看過(guò)一部分同學(xué)未通過(guò)面試的原因是:不管熟悉不熟悉的技術(shù)棧都一股腦兒地寫(xiě)到簡(jiǎn)歷中,結(jié)果面試時(shí)被問(wèn)到又說(shuō)不清楚。
三、校招看基礎(chǔ),社招看經(jīng)驗(yàn)
基礎(chǔ)知識(shí)就那么多,包括算法與數(shù)據(jù)結(jié)構(gòu)、操作系統(tǒng)原理、計(jì)算機(jī)網(wǎng)絡(luò)(網(wǎng)絡(luò)編程)、多線程、數(shù)據(jù)庫(kù)、設(shè)計(jì)模式等等;社招看經(jīng)驗(yàn),所謂經(jīng)驗(yàn),不僅指良好的基本功,還包括豐富的項(xiàng)目經(jīng)驗(yàn)和解決問(wèn)題的能力,一般 2-1 及以上職級(jí)的基本上要求至少擅長(zhǎng)分布式、RPC、消息中間件、緩存、數(shù)據(jù)庫(kù)等至少其中的一種。另外,就是解決問(wèn)題的能力,很多算法題或者場(chǎng)景題都是源自于真實(shí)的業(yè)務(wù)場(chǎng)景,需要面試者給出自己解決問(wèn)題的思路和可以落地的方法。
四、字節(jié)很注重算法能力
通常情況下, 對(duì)于校招生,算法題做不好,基本一票否決;對(duì)于社招,工作五年及五年以下的,也會(huì)考察一部分算法題。很多工作多年的同學(xué),由于平時(shí)不注重溫習(xí)和理解算法與數(shù)據(jù)結(jié)構(gòu)知識(shí),忘記了很多算法思想和解決問(wèn)題的策略。給這部分同學(xué)的建議是:
- 面試前適當(dāng)復(fù)習(xí)下常見(jiàn)的算法與數(shù)據(jù)結(jié)構(gòu);
- 另外,平??桃馊ニ⒁恍┧惴},去鍛煉一下自己的解決問(wèn)題的思路;
- 面試的時(shí)候,如果遇到不會(huì)的算法題,千萬(wàn)不要直接放棄,可以先嘗試暴力窮舉法或者找面試官要些提示。
除了一些算法崗位,社招的算法題一般都不難,有些算法題也不單純是考察算法,可能結(jié)合其他知識(shí)點(diǎn)一起考察,這里列舉一些題目給讀者做一些參考:
- 1. 實(shí)現(xiàn)一個(gè)字符串轉(zhuǎn)換整數(shù)的函數(shù);
- 2. 輸入兩個(gè)遞增排序的鏈表,合并這兩個(gè)鏈表并使新鏈表中的結(jié)點(diǎn)仍然是按照遞增排序的,例如:
- 鏈表1:1 -> 3 -> 5 -> 7
- 鏈表2: 2 -> 4 -> 6 -> 8
- 合并后的鏈表3:
- 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8
- 鏈表定義:
- struct ListNode
- {
- int m_nValuel
- ListNode* m_pNext;
- };
- 3. 輸入n個(gè)整數(shù),找出其中最小的 k 個(gè)數(shù)目,例如輸入4、5、1、6、2、7、3、8,則最小的4個(gè)數(shù)字是1、2、3、4。
- 4. 輸入兩個(gè)鏈表,找出它們的第一個(gè)公共結(jié)點(diǎn),鏈表結(jié)點(diǎn)定義如下:
- struct ListNode
- {
- int m_nKey;
- ListNode* m_pNext;
- };
- 5. TCP的滑動(dòng)窗口機(jī)制知道嗎?設(shè)計(jì)一個(gè)可行的滑動(dòng)窗口的算法。
- 6. 中國(guó)象棋中,假設(shè)左下角的位置為坐標(biāo)原點(diǎn),某棋子馬的坐標(biāo)是(x, y),另外一個(gè)棋子的坐標(biāo)為(m, n),實(shí)現(xiàn)一個(gè)函數(shù)返回馬下一步可走的位置坐標(biāo)。
- 7. 實(shí)現(xiàn)一個(gè)緩沖區(qū)類,需要支持以下功能:
- (1). 緩沖區(qū)內(nèi)存要求連續(xù)
- (2). 支持?jǐn)U容
- (3). 支持讀和寫(xiě)
如果你對(duì)這些算法題有不明白的地方,可以通過(guò)我的公眾號(hào)加我的微信群交流。
上述題目部分是《劍指 offer》一書(shū)的原題,我向讀者推薦一下這本書(shū),另外一本是左程云老師的《程序員代碼面試指南》。
適當(dāng)刷一些算法題,不單純?yōu)榱藨?yīng)付面試,對(duì)鍛煉思維能力也是大有裨益的。
當(dāng)然,很多非科班的同學(xué)一上來(lái)就刷算法題,其實(shí)是不推薦的,如果你沒(méi)有系統(tǒng)地學(xué)習(xí)過(guò)算法和數(shù)據(jù)結(jié)構(gòu)的課程,建議先系統(tǒng)地學(xué)習(xí)下這一塊的內(nèi)容。
五、字節(jié)很注重動(dòng)手能力
疫情仍然沒(méi)有完全過(guò)去,現(xiàn)在字節(jié)的面試基本上都是改在線上進(jìn)行,當(dāng)面試官給你一些算法題或者場(chǎng)景題時(shí),需要你在自己的電腦上進(jìn)行編程,在編程過(guò)程中的各種行為,例如你的編寫(xiě)代碼、解決編譯錯(cuò)誤、調(diào)試能力和代碼風(fēng)格都是面試官一眼能看到的。平常動(dòng)手多寡、高下立判,我曾遇到用 VSCode 寫(xiě) Java 代碼然后連編譯問(wèn)題都解決不了的同學(xué),顯然,最終面試肯定也未通過(guò)。
六、網(wǎng)上的面經(jīng)適當(dāng)看,帶著理解與批判精神去看
面試大廠的結(jié)果有一定的運(yùn)氣成分,不同的人在不同的場(chǎng)景面試遇到不同的面試官其表現(xiàn)的面試結(jié)果可能也不一樣。
近來(lái)我發(fā)現(xiàn)有些校招的同學(xué),把網(wǎng)絡(luò)上的面經(jīng)和所謂的標(biāo)準(zhǔn)答案一字不拉地背誦下來(lái)用于應(yīng)付面試,這是非常不可取的:
其一,有些面筋的答案本身就是錯(cuò)的,例如網(wǎng)上有一篇流傳很廣的文章,談到 HTTP GET 與 POST 請(qǐng)求的區(qū)別時(shí),說(shuō) GET 請(qǐng)求會(huì)發(fā)送一個(gè)數(shù)據(jù)包,而 POST 請(qǐng)求則會(huì)拆成兩個(gè)數(shù)據(jù)包去發(fā)送,這種說(shuō)法明顯就是錯(cuò)誤的。很多同學(xué)不加甄別的背誦下來(lái),我迄今至少遇到兩位同學(xué)面試時(shí)這么回答;
其二,光背面經(jīng)如果不加以理解很難應(yīng)付靈活變化的面試題,例如,有些同學(xué)把三四握手和四次揮手的過(guò)程背誦下來(lái)了,但是當(dāng)我問(wèn)到連接一個(gè) IP 不存在的主機(jī)時(shí),握手過(guò)程是怎樣的、或者連接一個(gè) IP 地址存在但端口號(hào)不存在的主機(jī)握手過(guò)程又是怎樣的呢?如果對(duì)三次握手過(guò)程不加以理解,是很難回答出這樣的問(wèn)題的。企業(yè)需要一些理解技術(shù)原理并能靈活運(yùn)用的員工,而不是死記硬背的人。
七、C++ 和 Java 太難了,直接學(xué) Go 吧
很多同學(xué)覺(jué)得 C++ 太難了,學(xué)不好,Java 學(xué)的人太多,競(jìng)爭(zhēng)壓力大,所以干脆學(xué) Go 吧,上文說(shuō)過(guò),各大招 Go 崗位的公司其實(shí)對(duì) Go 本身不做刻意要求,反而對(duì)技術(shù)原理要求不低。所以,打鐵還得自身硬,想靠學(xué) Go 走捷徑其實(shí)行不通,技術(shù)基本功決定著你將來(lái)在技術(shù)這條路上能走多遠(yuǎn)。那些看似難啃的技術(shù)原理,今天所欠下的技術(shù)債,總會(huì)在你的職業(yè)生涯的某一個(gè)階段爆發(fā)出來(lái)。以學(xué)習(xí) C/C++ 為例,如果你學(xué)習(xí) C/C++ 單純只是為了找工作或者應(yīng)付面試,那一定也是學(xué)不好的,C++ 語(yǔ)法本身并沒(méi)有多難,難的是支持 C++ 技術(shù)背后的各種操作系統(tǒng)原理,這些原理你在 C/C++ 中會(huì)用到,你在學(xué)習(xí)其他語(yǔ)言到一定階段也不可或缺;反過(guò)來(lái),以網(wǎng)絡(luò)編程為例,在 Go 中討論 epoll 模型多少有點(diǎn)別捏,或者是說(shuō)不清道不明,但是站在 C/C++ 的角度結(jié)合操作系統(tǒng)的網(wǎng)絡(luò) API,這個(gè)問(wèn)題就很容易搞明白了。
所以,對(duì)于開(kāi)發(fā)這條路來(lái)說(shuō),換一門(mén)容易學(xué)的語(yǔ)言并不能讓你擁有核心競(jìng)爭(zhēng)力。相反,如果你想做好開(kāi)發(fā),尤其是后端開(kāi)發(fā),你應(yīng)該掌握一門(mén)重型編程語(yǔ)言,如 C++ 或者 Java,這也是為什么我勸那些想做好開(kāi)發(fā)的同學(xué)不要只掌握一門(mén) Python/PHP 這樣的語(yǔ)言。