境自檢與冒煙測(cè)試)
每個(gè)程序員的電腦里都住著一行 Hello world!。不管是寫前端的、做嵌入式的還是半路出家搞數(shù)據(jù)分析的第一次敲下這行代碼、按下運(yùn)行鍵、盯著終端里的輸出愣住兩秒的時(shí)刻基本可以算作職業(yè)生涯里的第一個(gè)高光瞬間。這行只有兩個(gè)單詞的小程序看起來簡(jiǎn)單到不值一提但它背后藏著的是編程里最核心的一整套邏輯寫代碼、編譯或解釋、運(yùn)行、輸出、退出。這篇文章想聊的就是這個(gè) Hello world! 本身——它從哪來為什么所有課程都拿它當(dāng)?shù)谝徽n它在不同語言里有什么方言以及新手最容易在它身上踩到哪些坑。不夸張地說把它徹底搞明白你就已經(jīng)摸到了程序世界的大門。1. Hello world! 從哪來一段被誤讀的起源1.1 上世紀(jì) 70 年代的實(shí)驗(yàn)室往事很多人以為 Hello world! 是跟著個(gè)人電腦一起出現(xiàn)的其實(shí)它的歷史比這還要早個(gè)十幾年。上世紀(jì) 70 年代初某通信研究機(jī)構(gòu)里有一批工程師在做一門新的系統(tǒng)編程語言配套要寫一份內(nèi)部培訓(xùn)手冊(cè)。手冊(cè)需要示例代碼于是有位工程師隨手寫了一個(gè)向終端輸出一句問候的小程序用的詞就是 Hello world!。當(dāng)時(shí)的終端還沒有圖形界面所有交互都靠命令行完成程序能做的事也只有把一句話打印到屏幕上這一件。這個(gè)細(xì)節(jié)在當(dāng)時(shí)毫不起眼直到 1978 年那本影響了整整一代人的 C 語言入門教材出版時(shí)編寫者為了讓第一個(gè)示例足夠簡(jiǎn)單、又能立刻展示程序能產(chǎn)生輸出再次沿用了這行話。從那以后Hello world! 就像被寫進(jìn)了行業(yè)基因一代接一代地傳了下來。需要說明的是這段歷史在流傳中版本很多有人說是 1972 年有人說是 1974 年準(zhǔn)確的年份和具體的功勞歸屬其實(shí)沒那么重要。重要的是它傳達(dá)了一個(gè)信號(hào)最經(jīng)典的東西往往誕生于最樸素的場(chǎng)景——一個(gè)工程師只是需要一行能跑通的代碼而已。后來各種編程書、在線教程、視頻課程都照著這個(gè)套路來等于大家在無意中完成了同一件事給每個(gè)新手一個(gè)最低成本的起點(diǎn)。1.2 為什么偏偏是這兩個(gè)單詞那為什么是 Hello world! 而不是 Hello everyone、Good morning 之類我自己的理解有三點(diǎn)。第一足夠短兩個(gè)單詞加一個(gè)感嘆號(hào)手打不容易出錯(cuò)就算初學(xué)者一個(gè)字母一個(gè)字母摳也能完成第二語義足夠自然程序第一次運(yùn)行就像你剛學(xué)會(huì)一門外語開口第一句總得是打招呼第三它恰好引出了編程里最基礎(chǔ)的概念——字符串。Hello 是字符串空格的另一邊 world! 也是字符串的一部分這句話能順理成章地告訴新手帶引號(hào)的文本就是字符串程序輸出的是文字而不是別的什么。還有一個(gè)細(xì)節(jié)很多人沒注意后來不少書把它寫成帶逗號(hào)的 Hello, World!有的則沿用不帶逗號(hào)的 Hello world!。這讓初學(xué)者在網(wǎng)上搜代碼時(shí)經(jīng)常感到困惑其實(shí)兩種寫法都對(duì)關(guān)鍵是你跟隨的教材保持統(tǒng)一就行。到了今天它已經(jīng)變成了一種文化符號(hào)甚至非程序員也認(rèn)識(shí)這句話。1.3 一段小小的考古它比圖形界面還老再往前翻其實(shí)更古老的機(jī)器演示程序也有類似傳統(tǒng)。上世紀(jì)五六十年代的大型機(jī)時(shí)代工程師們就習(xí)慣用一個(gè)極短的程序來驗(yàn)證機(jī)器能不能正常工作只是當(dāng)時(shí)的輸出選題往往是數(shù)字、字母表或者自己所在機(jī)構(gòu)的名字。Hello world! 之所以能從這些零散傳統(tǒng)里勝出靠的是它獨(dú)一無二的擬人感——你在向整個(gè)世界打招呼而系統(tǒng)回應(yīng)了你。這種反饋在命令行時(shí)代特別珍貴因?yàn)槟莻€(gè)年代人和機(jī)器交流的全部體驗(yàn)就只有終端上滾動(dòng)的字符?;仡^看這行代碼能活到今天靠的不只是技術(shù)上的簡(jiǎn)單更是情感上的共鳴。2. 為什么幾乎所有入門課程都拿它開刀2.1 最小可運(yùn)行程序一次完整的溝通閉環(huán)編程入門最難的往往不是某個(gè)語法而是腦子里沒有一個(gè)完整的程序是怎么跑起來的模型。Hello world! 的價(jià)值在于它是你能寫出來的最小的、可運(yùn)行的、有輸出的完整程序。我常用一個(gè)比喻你寫 Hello world! 的過程相當(dāng)于給計(jì)算機(jī)遞了一張便條上面寫著請(qǐng)把這句話原樣顯示在屏幕上。計(jì)算機(jī)讀完便條理解了你的要求然后真的照做了。這一來一回就是一個(gè)完整的溝通閉環(huán)。很多教材會(huì)跳過這個(gè)閉環(huán)上來就教變量、循環(huán)、函數(shù)結(jié)果初學(xué)者學(xué)了兩個(gè)小時(shí)還停在我到底在干嘛的困惑里。而 Hello world! 五分鐘內(nèi)就能讓你親眼看到我寫的代碼變成了屏幕上實(shí)實(shí)在在的字符。這個(gè)正反饋一旦建立后面學(xué)什么都順。我自己帶過幾個(gè)零基礎(chǔ)的朋友無一例外第一次成功輸出時(shí)的表情都帶著點(diǎn)驚喜。那不只是技術(shù)上的成功更是一種我居然能指揮計(jì)算機(jī)的掌控感。2.2 環(huán)境自檢新電腦、新語言的第一塊試金石Hello world! 的第二個(gè)實(shí)際用途是驗(yàn)證開發(fā)環(huán)境。在我個(gè)人看來這是它最被低估的功能。換了一臺(tái)新電腦裝了一個(gè)新版本的編譯器或者配了一套新的集成開發(fā)環(huán)境你要做的第一件事永遠(yuǎn)不是寫業(yè)務(wù)邏輯而是跑一個(gè) Hello world!。因?yàn)樗芤淮涡员┞端凶畛R姷膯栴}解釋器沒裝好、環(huán)境變量沒配對(duì)、編輯器保存格式不對(duì)、編譯命令敲錯(cuò)、缺了某個(gè)依賴庫。只要 Hello world! 能跑通說明你的工具鏈基本是健康的之后寫任何代碼都不會(huì)莫名其妙地卡在環(huán)境上。我自己有個(gè)習(xí)慣會(huì)在新環(huán)境里依次跑三件事跑 Hello world!、打印當(dāng)前語言版本號(hào)、輸出系統(tǒng)信息。三步都正常我才會(huì)開始干正事??雌饋矶嗷巳昼妼?shí)際上能省下后面好幾個(gè)小時(shí)的排錯(cuò)時(shí)間。尤其是團(tuán)隊(duì)協(xié)作場(chǎng)景新人入職時(shí)配置環(huán)境的環(huán)節(jié)一個(gè) Hello world! 測(cè)試腳本能省去無數(shù)輪我這里跑不了的來回溝通。2.3 為什么不直接教更實(shí)用的例子也有人會(huì)問既然要讓新手盡快上手為什么不直接寫一個(gè)算數(shù)程序、網(wǎng)頁小程序或者更貼近生活的例子原因很簡(jiǎn)單那些例子背后的依賴太多了。一個(gè)網(wǎng)頁版的 Hello world! 至少涉及服務(wù)器、端口、瀏覽器、前端渲染四層任何一層出問題新手都無法判斷是自己代碼錯(cuò)了還是環(huán)境錯(cuò)了。算數(shù)程序雖然邏輯簡(jiǎn)單但它缺少程序在主動(dòng)做某件事的直觀感受。Hello world! 的高明之處在于它把所有噪聲都屏蔽掉了不需要網(wǎng)絡(luò)不需要額外依賴不需要理解復(fù)雜的庫調(diào)用。你面對(duì)的只有源碼、編譯器或解釋器、終端輸出三個(gè)對(duì)象。在一個(gè)信息極度過載的世界里能讓新手在五分鐘內(nèi)獲得一次純粹的、成功的體驗(yàn)這本身就是最好的教學(xué)設(shè)計(jì)。3. 同一行文字在不同語言里的方言3.1 編譯型語言C、Go、Rust 的寫法差異先說編譯型語言。這類語言的代碼要先經(jīng)過編譯器翻譯成機(jī)器能懂的指令再生成一個(gè)可執(zhí)行文件之后才能運(yùn)行。以 C 語言為例#include stdio.h int main(void) { printf(Hello world!\n); return 0; }這段代碼里#include stdio.h是把系統(tǒng)自帶的輸入輸出工具引進(jìn)來int main(void)是程序入口printf負(fù)責(zé)打印return 0告訴操作系統(tǒng)程序正常結(jié)束。很啰嗦但每一步都清清楚楚沒有任何隱藏動(dòng)作。再看 Gopackage main import fmt func main() { fmt.Println(Hello world!) }Go 要求先聲明package main表示這是一個(gè)可執(zhí)行的程序包然后從func main()開始執(zhí)行輸出函數(shù)是fmt.Println結(jié)尾也不用寫 return 0默認(rèn)正常退出。Rust 則更簡(jiǎn)潔一點(diǎn)fn main() { println!(Hello world!); }這里沒有包聲明沒有引入外部庫輸出用的是帶感嘆號(hào)的宏println!。三種語言都在做同一件事但對(duì)外部依賴、入口約定、函數(shù)命名都有自己的規(guī)矩。這就是語言設(shè)計(jì)哲學(xué)的差異C 讓你看見一切細(xì)節(jié)Go 強(qiáng)調(diào)規(guī)范和內(nèi)置工具Rust 把省心和內(nèi)存安全放在第一位。你從這幾個(gè)例子就能初步感受到學(xué)一門新語言時(shí)真正要學(xué)的不是打印這句話而是這門語言的人是怎么想問題的。3.2 解釋型與腳本語言Python、JavaScript、Shell再看解釋型語言和腳本語言。它們不需要事先編譯直接把源碼丟給解釋器就能執(zhí)行。Python 的寫法大概是所有語言里最省事的print(Hello world!)JavaScript 在服務(wù)端運(yùn)行時(shí)環(huán)境里長這樣console.log(Hello world!);Shell 里就更直接了echo Hello world!這些語言把復(fù)雜度藏了起來代價(jià)是運(yùn)行效率通常不如編譯型語言而且錯(cuò)誤往往要等到運(yùn)行那一刻才暴露。對(duì)新手來說解釋型語言最大的好處是反饋快改一行立刻重跑沒有任何中間步驟。所以我建議完全零基礎(chǔ)的人先從 Python 這類語言入門先建立寫代碼、看結(jié)果的肌肉記憶再去碰編譯型語言里的那些底層細(xì)節(jié)。3.3 先別急著抄先看懂這幾點(diǎn)差異光抄代碼沒意義我拿一個(gè)表格把常見語言的差異列出來對(duì)照著看更直觀語言類型程序入口輸出方式句尾分號(hào)C編譯型int main(void)printf必須Go編譯型func main()fmt.Println不需要Rust編譯型fn main()println!不需要Java編譯型public static void mainSystem.out.println必須Python解釋型文件頂層print不需要JavaScript解釋型頂層console.log可選Shell腳本頂層echo不需要看出規(guī)律了嗎需要分號(hào)的語言往往是那些需要明確一條語句到此結(jié)束的編譯型語言不需要分號(hào)的語言則靠換行或縮進(jìn)來判斷邊界。這不是隨便定的而是語言設(shè)計(jì)者對(duì)程序員應(yīng)該顯式還是隱式這個(gè)問題的不同回答。理解到這一層你再看任何新語言都會(huì)比別人快很多。另外注意 Java 的入口函數(shù)有一長串修飾符這是因?yàn)?Java 規(guī)定程序入口必須是某個(gè)類的公有靜態(tài)方法理解這個(gè)前提之后那一長串就不算難記了。4. 新手最容易栽的坑從Hello world!開始就在發(fā)生4.1 標(biāo)點(diǎn)符號(hào)的玄機(jī)分號(hào)、引號(hào)、括號(hào)我見過太多人卡在第一個(gè)示例上不是因?yàn)槟X子不靈光而是因?yàn)闃?biāo)點(diǎn)。最常見的問題包括字符串兩邊的引號(hào)只寫了一半、漏寫了分號(hào)、函數(shù)后面的括號(hào)不匹配。比如把printf(Hello world!\n);末尾的分號(hào)去掉printf(Hello world!\n)編譯器立刻報(bào)錯(cuò)。新手往往慌得不行其實(shí)解法非常機(jī)械從報(bào)錯(cuò)信息指出的那一行往上翻逐個(gè)檢查引號(hào)、括號(hào)、分號(hào)是否配對(duì)。我到現(xiàn)在調(diào)代碼最喜歡的依然是從標(biāo)點(diǎn)開始查起因?yàn)檫@是最高頻的錯(cuò)誤來源沒有之一。另一個(gè)容易忽略的點(diǎn)是縮進(jìn)。Python 尤其典型下面這段代碼會(huì)直接報(bào)縮進(jìn)錯(cuò)誤print(Hello world!) print(Hello again!)縮進(jìn)不是裝飾是語法的一部分。你可以不習(xí)慣它但不能不尊重它。類似的還有把函數(shù)名print寫成Print、把printf寫成print f這類大小寫和空格錯(cuò)誤在編譯型語言里尤其致命因?yàn)榫幾g器的錯(cuò)誤提示有時(shí)指向行號(hào)而不是具體字符新手經(jīng)常被繞得團(tuán)團(tuán)轉(zhuǎn)。4.2 全角字符與中文引號(hào)的陷阱這個(gè)坑非常中國特色。很多人用中文輸入法寫代碼結(jié)果把英文引號(hào)打成了中文引號(hào)“ ” 把逗號(hào)打成了全角逗號(hào)分號(hào)打成了全角分號(hào)。在混合使用輸入法的狀態(tài)下這種錯(cuò)誤極其隱蔽。比如print(“Hello world!”)看起來和正常代碼幾乎一樣但解釋器直接報(bào)語法錯(cuò)誤。我建議所有新手在寫代碼時(shí)把輸入法切到英文半角狀態(tài)或者直接用代碼編輯器自帶的自動(dòng)切換輸入法功能。這個(gè)習(xí)慣越早養(yǎng)成后面踩的坑越少。判斷標(biāo)準(zhǔn)也很簡(jiǎn)單如果英文半角狀態(tài)下的雙引號(hào)是直的中文全角狀態(tài)下是彎的“一眼就能分辨。4.3 輸出亂碼編碼問題第一次找上門如果你在中文系統(tǒng)里嘗試輸出你好世界大概率會(huì)看到一堆亂碼。這其實(shí)是所有中文開發(fā)者遲早會(huì)面對(duì)的編碼問題只是它往往在第一個(gè)程序里就出現(xiàn)。原因不復(fù)雜程序輸出的字符要按某種編碼規(guī)則解釋而不同系統(tǒng)默認(rèn)的編碼可能不一樣。主流桌面系統(tǒng)的默認(rèn)字符集和開源系統(tǒng)默認(rèn)的 UTF-8 不完全一致兩邊強(qiáng)行碰頭就會(huì)出現(xiàn)字符錯(cuò)位。解決辦法也很常規(guī)確保源碼文件以 UTF-8 保存并在終端里把字符集切到 UTF-8。亂碼問題在 Hello world! 階段解決掉后面處理文件、數(shù)據(jù)庫、網(wǎng)頁傳輸都會(huì)省心很多。我見過不少項(xiàng)目線上出了問題一路排查到數(shù)據(jù)庫連接層最后發(fā)現(xiàn)根源就是最初寫原型時(shí)約定的編碼不統(tǒng)一。編碼這個(gè)東西越早建立標(biāo)準(zhǔn)越省事。4.4 程序成功運(yùn)行不代表一切正常最后提醒一個(gè)概念程序能跑起來不等于程序是對(duì)的。以換行為例printf(Hello world!\n);和printf(Hello world!);在大多數(shù)場(chǎng)景下差別不大可一旦你做日志處理、管道流處理換行符就變成了關(guān)鍵差異。更深一層是退出碼。一個(gè)完整跑完的程序最終會(huì)向系統(tǒng)返回一個(gè)退出碼0 通常代表正常非 0 代表有異常。在終端里寫完 Hello world! 之后試著執(zhí)行echo $?類 Unix 環(huán)境查看退出碼大概率會(huì)看到 0——這其實(shí)就是程序在向你匯報(bào)我一切正常。養(yǎng)成看退出碼的習(xí)慣會(huì)有助于盡早發(fā)現(xiàn)那些看起來成功、實(shí)際上失敗的隱患。很多專業(yè)腳本會(huì)在關(guān)鍵步驟后檢查退出碼這個(gè)習(xí)慣可以追溯到你寫 Hello world! 的那一天。5. 別小看它把 Hello world! 變成一把萬能探針5.1 用最簡(jiǎn)單的程序驗(yàn)證工具鏈前面提到Hello world! 是環(huán)境自檢的第一塊試金石。這里我把它說得更具體一點(diǎn)。假設(shè)你剛配置好一個(gè)新的開發(fā)環(huán)境不要急著開大項(xiàng)目先跑一個(gè)最簡(jiǎn)單的程序并按照下面的清單逐項(xiàng)確認(rèn)編輯器能正常保存源碼文件后綴名正確命令行里能調(diào)用編譯器或解釋器如果是編譯型語言能順利編譯出可執(zhí)行文件運(yùn)行后能在預(yù)期的地方看到輸出退出碼為 0。任何一步失敗都在告訴你環(huán)境里某個(gè)環(huán)節(jié)有問題而排查范圍已經(jīng)被壓縮得很小了。我見過不少同事在環(huán)境沒配好的情況下硬著頭皮寫大項(xiàng)目結(jié)果報(bào)錯(cuò)滿天飛最后才發(fā)現(xiàn)是環(huán)境變量的問題。先用 Hello world! 把環(huán)境問題排掉是大項(xiàng)目開工前最劃算的一筆時(shí)間投資。更進(jìn)一步你甚至可以把這條命令寫進(jìn)自動(dòng)化腳本作為每次環(huán)境配置后的自檢步驟團(tuán)隊(duì)里所有人都能受益。5.2 從打印一句話到與程序?qū)υ挳?dāng)你能熟練地寫出 Hello world! 之后下一步才是關(guān)鍵讓它從一句自言自語變成一次對(duì)話。最簡(jiǎn)單的進(jìn)階是讓程序讀取你輸入的內(nèi)容。以 Python 為例name input(Whats your name? ) print(Hello, name !)運(yùn)行之后程序會(huì)停下來等你輸入一個(gè)名字然后回一句問候。這一步雖然只多了一行input但意義完全不同程序不再只是單向輸出而是開始接收信息、做出響應(yīng)。再往下走你可以試著從命令行參數(shù)里讀取內(nèi)容比如用 C 語言讀取argv#include stdio.h int main(int argc, char *argv[]) { if (argc 1) { printf(Hello, %s!\n, argv[1]); } else { printf(Hello world!\n); } return 0; }編譯后運(yùn)行./hello 程序員屏幕上就會(huì)變成Hello, 程序員!。到這里程序已經(jīng)從只會(huì)說一句話變成了會(huì)根據(jù)輸入改變行為。別小看這兩個(gè)例子它們背后分別是輸入輸出、變量、條件判斷、命令行參數(shù)——幾乎覆蓋了后續(xù)所有編程知識(shí)的根。5.3 實(shí)戰(zhàn)里它還能當(dāng)冒煙測(cè)試再往后走你會(huì)在真實(shí)的工程場(chǎng)景里重新遇見 Hello world!只是它換了身份叫冒煙測(cè)試。拿后端服務(wù)來說最穩(wěn)妥的起步方式就是先搭一個(gè)極小的接口訪問任意路徑就返回一行 Hello world!。這個(gè)接口能通說明網(wǎng)絡(luò)鏈路、框架路由、日志輸出都是好的然后你才往這個(gè)骨架上加數(shù)據(jù)庫、加緩存、加業(yè)務(wù)邏輯。一旦后面出了問題你永遠(yuǎn)能退回到那個(gè)最小的 Hello world! 版本去判斷是新增的模塊壞了還是基礎(chǔ)環(huán)境一直就沒好過。這個(gè)思路套用到任何領(lǐng)域都成立寫腳本也好配服務(wù)器也好做嵌入式也好永遠(yuǎn)保留一個(gè)最小可運(yùn)行的版本作為底牌。Hello world! 教會(huì)我們的遠(yuǎn)不止輸出一句話而是一種極其重要的工程思維——復(fù)雜系統(tǒng)要從最小閉環(huán)開始搭建并且時(shí)刻留有一條后退的路。我自己的習(xí)慣是把各種語言的 Hello world! 模板整理成一個(gè)筆記每次接觸新語言、新框架、新環(huán)境第一件事就是跑對(duì)應(yīng)的模板。說實(shí)話這些年它幫我省下的排錯(cuò)時(shí)間比任何技巧都多。最后再分享一個(gè)小技巧不要只把 Hello world! 當(dāng)成第一次的儀式多寫幾遍改一改引號(hào)、去掉換行符、加一個(gè)循環(huán)讓它打印十遍這些看似無聊的改動(dòng)其實(shí)是在幫你的手指和大腦建立編程的語感。等你哪天真能用一行代碼難住別人再回頭看那句 Hello world!你會(huì)發(fā)現(xiàn)自己已經(jīng)走出很遠(yuǎn)了。