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