https://zhuanlan.zhihu.com/p/26222486node
******************************python
上文談到了像讀書同樣閱讀源碼的重要性,今天談談如何閱讀一份代碼。我所謂的一份代碼,其範圍可能從幾千行到數萬行,有時甚至可多達數十萬行。這些代碼做爲一個有機體,共同完成某些重要的功能。好比說幾個著名的 full fledged web framework,祖師爺 rails,師叔 django 和小師妹 phoenix:linux
三者對比頗有意思 - rails / django 的代碼數都在 200k 上下,而 phoenix 小了整整一個數量級,僅僅在 20k 左右。實現大體相同的功能,語言的表現力難道差距如此之大?這解釋不通。實際上 phoenix 實現的功能,和 rails 對標,應該是 actionpack/actionview 二者加起來約 80k 的代碼。而 rails 內嵌的 activemodel/activerecord 應該對標 elixir 的 ecto,恰巧又是 80k 比 20k。這種差別反映了語言表現力的差別,同時也體現了框架的成熟度的區別。nginx
幾十 k 體量的代碼,就像一本不薄不厚的書,讀着既不算太過吃力,也不至於讀完意猶未盡。git
有些巨型的代碼庫,如 linux kernel,塊頭堪比『戰爭與和平』,代碼的規模宏偉到使人絕望,大大超過了咱們可以閱讀理解的範圍。其結果是咱們往往下定決心閱讀,投入的巨大的精力,卻像往一池湖水裏投個石子,雖掀起一絲漣漪,但湖面很快就歸於平靜。程序員
讀不下來,咱們也不要太絕望,能夠分而治之,先定個小目標,每次讀一部分,好比 scheduler(20k)或者 memory management(80k)。github
在選定合理的代碼規模和要閱讀的源碼後,咱們就能夠清開書桌,擺上 mac,準備好筆墨紙硯,留出至少一個小時到半天時間,開始徜徉在代碼的海洋。web
因爲上一篇文章(爲何咱們要閱讀源碼?)從頭到尾將閱讀書籍和閱讀代碼進行對比,不少人會不由聯想本文會否和『如何閱讀一本書』進行類比,提供一樣的思路:基礎閱讀,檢視閱讀,分析閱讀,對比閱讀。不錯,這些讀書的方法對讀代碼很是有參考價值,好比說檢視閱讀,咱們讀大型代碼項目,也是相似的思路:redis
咱們會讀根目錄下的 readme,或者任何看上去撩撥着你讓你戳它的文件。這就跟書本的「序」同樣,可以幫你更進一步瞭解這份代碼的意圖;算法
接下里咱們目光須要聚焦在代碼的目錄結構,和每一個源碼的文件名。他們就像書本的目錄頁。若是每一個目錄下有 readme,也可快速閱讀之。不少語言和框架,有約定俗成的目錄結構(Convention by Contract),所以,經過目錄咱們就能夠快速知道哪些是能夠略過的部分,好比 django 的 management/commands 目錄,elixir 的 mix/tasks 目錄,這些目錄,承載着支線劇情,須要的時候,或者閒得無聊時再讀也不遲;
而後咱們開始從入口梳理主線。不一樣語言和框架的主線不太同樣,好比 C 的入口是 main(),erlang/elixir otp app 的入口是 app:start,nodejs 每每是根目錄下的 index.js,等等。通常而言,application 的主線比較清晰,一路下來,會走到一個 mainloop,而 framework 的主線會晦澀一些,由於 framework 每每是 application 抽象出來的部分。
但本文不會過多這樣去對比 —— 你們真有興趣,何不親自讀讀那本書(讀過的請帶着讀代碼的角度再讀一遍)呢?畢竟,它更加完備,更加系統。
我想經過另外一個角度 —— 閱讀的場景來談談如何閱讀。談到場景,不少人會聯想到一本著名的書:Linux 內核場景分析。該書的做者顯然把握了閱讀代碼的實質:循着一條線索,進行端到端的一個自成體系的內容的閱讀。不一樣場景下,咱們已知的信息,未知的信息,經過閱讀想要達到的目標是不同的,顯然,方法也不盡相同。這就和讀書同樣:想要讓本身明智,讀史;想要讓本身靈秀,讀詩;想要讓本身周密,研習數學;想要讓本身深入,攻讀哲學等等。
接下來,本文就從若干閱讀代碼的場景開始,討論我的的讀代碼的一點微不足道的心得。
場景一:爲了破案而閱讀代碼
這是咱們最主要的閱讀代碼的場景。工做中,免不了用各類各樣的開源系統(別人的代碼)。使用的過程當中,你會遇到各類奇葩的問題,這些問題可能源於對文檔的理解不夠,或者從網上抄一個已有的,並不徹底適合你使用場景的樣例,或者是真的撞上了八阿哥。在千方百計解決的過程當中,若是同事幫不上忙,google/stackoverflow 不夠給力,論壇上各類「急,在線等」也無人理會,你會開始抓狂,彷彿被攝魂怪纏上了通常,生活中的各類美好,但願,都開始離你遠去。
這時,你不得不像 CSI 中的警探同樣,順着一點蛛絲馬跡,開始剖析代碼,試圖從迷霧中還原真相。你會拋開一切紛擾和雜音,集中精力,帶着線索,循着問題,讀且只讀和解決問題直接相關的代碼。這種狀態,我管它叫「獵手模式」—— 咱們有如非洲草原上追逐離羣斑馬的獅子,把身上燃起的小宇宙集中於一點,眼睛緊盯獵物奔走的方向,腿如疾風,勢如閃電,心中不斷地盤算着雷霆一擊究竟用鎖喉,打臉抑或拉後腿勝算更大。道路上的石子劃了腳,痛;飛奔時撞上了幺蛾子,煩,但這都不是事兒。就算遠遠的乞力馬扎羅山上可貴地掛上了兩道彩虹,煞是美麗,自拍後發朋友圈定能破百贊,你也無暇顧及這些並不重要的細節。
專一,集中力量攻擊且僅攻擊一點是這樣場景下閱讀代碼的主要方式。
拿我遇到的 nginx cache 的問題來舉個栗子。一年前,當我接手 Tubi TV 的性能較低且很難維護的 API 系統後,雖打定主意往後重寫,但擺在面前的,刻不容緩的問題是提升性能。應用層能夠施展的空間不大(數據已經在 redis 裏),因此只能在 web 層打主意。在 HAProxy 和 nginx cache 之間,我選擇了後者, 由於 nginx 已經在當時的生產環境下大量使用。我雖然沒用過 nginx cache,但啓用 nginx cache 並非難事,照着文檔設置好 cache 的路徑和大小等參數後,在須要使用 cache 的 location 下,設置 cache key 並使能便可,我本地的簡單的測試運行正常。然而,在生產環境中,本該命中的請求卻一直處在 miss 的狀態。我束手無策,嘗試了網上搜到的各類方案無果。最終,我決定本身編譯一個打開 DEBUG 開關的 nginx 版本(--with-debug),記錄更多的日誌,而後對着源碼找問題。
nginx cache 及 upstream 裏和 cache 相關的代碼量並不算多,幾千行,我快速過了一下,而後就着日誌上的內容尋找相關的處理流程,並在幾個大的 bailout 分支猜測可能出現的情景。因爲 nginx debug log 仍是不夠詳細到知足個人需求,我在這些沒有被顧及到的分支上各自加了調試代碼,從新編譯,運行。
這個過程當中,「猜」起到了很大的做用。我記得本科時的數學老師 —— 一個可愛的小老頭 —— 點名提問對方答不上來時,經常掛在嘴邊的口頭禪是:你猜一下嘛。他總說連蒙帶猜也是解題的一種思路,偉大的數學家同時也是偉大的猜測家。
咱們讀代碼時,猜文件名,函數名,變量名的意圖,猜某個分支的意圖,猜某段代碼的意圖,最終結合運行的結果,打印出來的調試信息來印證咱們的猜想。這是讀者和做者間有趣的貓鼠遊戲。讀得越多,猜得越多,印證得越多,造成一個有效的 feedback loop(read - guess - verify),你下次猜想成功的概率就越大。
最終,問題被我定位出來 —— 它是兩三處 configuration 未正確配置的問題。stackoverflow 上的答案是部分正確的,它解決了絕大多數人的問題 —— 沒有 ignore cache control 相關的 header 幾乎被每一個初次使用者忽略了,它也是個人配置問題之一。但之因此這個答案沒能解決個人問題,是由於咱們生產環境中的 nginx 有個不起眼的配置 disable 了 proxy buffer,從而致使 nginx 直接跳過 cache。
從以上的過程當中,咱們抽象一下,看看爲了破案而閱讀代碼要注意什麼:
帶着線索,從一堆代碼中找出和問題相關的代碼。nginx cache 的例子中,線索是 proxy 的 upstream,cache 總不能命中,因此出問題的代碼和 cache,proxy,upstream 有關。源碼目錄裏一翻,就能挑出須要看的文件。因爲問題在 cache 上,在挑出的文件中,具體看 cache 相關的函數名,宏名,以及代碼。
專一閱讀挑出來的內容,忽略不相干的噪音。在閱讀的過程當中,着力尋找潛在的觸發問題的路徑,而後動用「我猜我猜我猜猜猜」大法,加調試信息。
編譯運行修改過的代碼,復現問題,分析調試信息,而後,bingo,恭喜你答對了!請進入第 5 步。
沒答對,請回第 1 步。別急,這不是高考,在老闆忍無可忍炒你魷魚以前,你一直擁有再來一瓶的機會。
還有個關鍵的第 5 步,我單拎出來講。不少時候咱們輪迴數次,終於在第三步 bingo 後快樂地像是剛剛 K.O. 了對手的春麗,夾着腿跳將起來,左右手在空中一齊比劃着二,忘乎所以,以致於忘記了執行第 5 步。
喜悅是短暫的,記憶也是短暫的。整個過程你的目標是如此清晰,執行力無比強大,爲達到目的「不擇手段」。三天後老闆問你,小程啊,你很棒啊。你用了什麼手段征服了這個無比難纏的八阿哥?這時你拼命追憶,卻像拿篩子盛水,忙亂半天一無所得。你開始懷疑人生:三天前的我和如今的我到底是不是一我的?
因此關鍵的第 5 步是:覆盤。解決問題後,彆着急接受同事們的致謝和女(男)神的秋波。趁着那坨記憶還熱氣騰騰,抄起 evernote(或者 xxx),把整個過程用最簡潔的方式記錄下來 —— 關鍵代碼,關鍵路徑,到達終點的整個猜想過程,以及那些日誌驗證了猜想是對的,哪些日誌驗證了猜想走不通(恭喜你 —— console 或者 terminal 在這個時候應該還沒關),總之,你在不擇手段的過程當中用過的一切手段,都應該像記流水帳同樣記錄下來。最後分析總結:
這個問題的 root cause 是什麼?觸發它的代碼的流程是什麼?
在讀代碼的過程當中,哪些地方我猜對了,哪些沒猜對?
有功夫的話,代碼的哪一個部分是值得細細品讀把玩的?
下次再出現相似的問題,我該怎麼更快地從源碼中定位出問題?
在這種「破案」般閱讀代碼的歷程中,若是沒有覆盤,你 70% 的功夫白費了 —— 你花了很多時間,讀了很多代碼,除了一個好的結果外,並沒有太大的收穫。惋惜的是,絕大多數工做場景,咱們都略過了這步。我本身也是(深入反省中)。寫這個章節時,我搜了搜個人 evernote,翻了翻個人郵件,除了去年初有封郵件隻言片語介紹了我使能了 nginx cache 外,再無其餘記錄。好在我當時解決完問題順便又讀了些 nginx cache 的代碼,有些許印象,因此還能把它搬出來作例子。
覆盤幫你把這樣的信息沉澱下來,讓你有機會回顧,進而組織和固化成上篇文章中所說的知識。這樣的內容累積多了,慢慢你的頭上就會頂起一個光環,光環上傲嬌地寫着:磚家。
場景二:爲了明理而閱讀代碼
場景一所述的讀代碼方法是被動的,爲了對付問題而讀,是大部分人精進代碼的惟一途徑。這就比如英雄無敵裏你就作個守成之主,收收礦,屯屯兵,從不主動招惹野怪,只等着敵人來進攻。這樣三個月下來,就打了幾仗,穩則穩矣,無奈經驗值增加太慢。
要想漲快點怎麼辦?主動出擊啊!計算機領域的不少算法,基礎知識,理論,在看過書,讀過文章後咱們都似懂非懂,這時,閱讀代碼就是最快地鞏固和加深理解的方式:
算法:bloom filter 究竟怎麼實現的?怎麼樣把 bandit 算法在本身的系統上作簡單的推薦?ossip 協議實際的生產環境的代碼是什麼樣子的?Linux kernel 如何實現 O(1) scheduler?
基礎知識:一個完整的涵蓋 HTTP 1.1 協議的 REST API framework 如何實現?一個 packet 從 OS 的 driver 是如何一路送上 application 的?什麼是 zero copy?Linux kernel 如何實現 zero copy?
理論:啥是 IoC / DI / Pub Sub?各類 framework 都是咋實現這些設計模式的?supervisor 這個 behavior 背後的實現是如何的?
這個過程是一個正反饋,是馬太效應累積地過程。你讀的書多,你腦子裏的知識點就多,疑問一樣也多。這些疑問促使你讀相關的代碼去印證和解惑,代碼讀多了,又感受理論知識欠缺,因而周而復始,不斷學習下去。反之,書讀得少,你腦子裏都沒存幾個問號,也就無所謂讀代碼去求證了。
以 REST API framework 爲例 —— 兩年前我還在 Juniper 作 web security 時,須要作一個堅實的 API system。咱們知道作網絡的,幹起事來要比作互聯網嚴謹得多(因此也慢得多),因而我花了好些時間讀了 RFC 2616 及其後續的修訂(7230-7235)。而後就是對 API framework 進行選型,找個合適的。當時我正好在研究 clojure,便拿了 liberator 來看。Liberator 受 erlang 下的 webmachine 啓發,用簡單的 macro 把 decision tree 實現得很優雅。後來我又掃了下 webmachine 的 decision tree,pattern matching + 遞歸,很是漂亮。惋惜當時我在的團隊思想比較僵化,眼裏只容得下 python。無奈我退而求其次,選用了 eve,一個 python 下的 rest API framework。eve 的代碼質量中規中矩,平鋪直敘,明顯像是你我寫出的代碼的模樣。
詳細講講我讀 webmachine 的過程。我讀 webmachine,徹底是 liberator 的引薦,liberator 的做者說其 decision tree 來自於 webmachine,並附了圖。這時的我就像剛練了李小龍的截拳道,聽聞這功夫源自詠春,一會兒就心癢癢欲探探詠春的虛實了。
webmachine 的代碼很短,只有 4700 行。循着文件名很快就能找到 webmachine_decision_core.erl,這是要閱讀的主體內容,約 800 行。這 800 行代碼,咱們能夠將其分紅三個部分:頭 150 行,decision tree 的架子;中間 300 多行,是具體的一個個 decision 的實現;剩下的兩百行,是輔助函數。
每一個 decision 的流轉見 下圖:
這裏 atom 的命名徹底跟着圖走,比方說 v3b13 這個 atom,含義是 v3 版本的圖,b 列 13 行的 decision node。這是第一個 decision,若是 service available,則整個 flow 繼續往下走,不然返回 503 service unavailable。
明白了這一點,按圖索驥,代碼的執行流程很是好懂。接下來的事情就很簡單了 —— 順着流程一個一個看 decision node 的代碼,RFC 2616 變得鮮活起來,在你眼前跳動。咱們再看一個例子:precondition check,是 v3g11:
這段代碼從 http header 裏讀出 if-match header 裏的 etags 列表,而後經過 resource_call 調用 generate_etag,來生成 etag,和 etags 裏的任意一項匹配,若是匹配,跳轉到 v3h10,不然 412 preconditional failed。webmachine 怎麼知道如何生成 etag 呢?這即是 framework 的功力了,它抽取並實現協議的公共的部分,而將業務邏輯延遞給使用 framework 的 application。換句話說,generate_etag 是 application 要實現的 callback。這即是 IoC。
這個代碼解釋到這裏,明白 HTTP 協議中 etag 做用的人,或者對 concurrency control 方案清楚的人天然一目瞭然;但我相信很多人會很難理解它的應用場景。再進一步解釋一下:好比小明和小紅是程序人生的兩個管理員,他們經過 API 同時從數據庫中獲取到程序人生的基本信息(名稱,描述等) v1。小明把程序人生的名字改爲了「序員人生」,調用 PUT API 成功修改數據爲 v2。小紅也同時修改這個數據,但她仍是使用原有的 v1 的數據進行修改,結果提交時把小明的修改覆蓋了。這是個 concurrency control 的經典場景 —— 使人談虎色變的 race condition。怎麼辦(想一想你平時怎麼處理)?HTTP 協議的處理辦法是:小明和小紅拿數據的時候,同時拿到一個「版本號」(你就想象成數據的 sha1),這裏管它叫 etag。小明更改後,數據的 etag 變了,小紅拿舊的 etag 提交時,服務器一檢查當前的 etag,不匹配,因而便 412 了。這是一個簡化了的 optimistic lock。
拉拉雜雜說這麼多,只想說明一件事:可以讀懂代碼,和理解代碼的應用場景是兩碼事。可是當你真正理解以後,你的代碼功力就大漲。往後作併發環境下的共享對象更新,你腦殼裏會閃個問號:嚇,這裏可不能夠不用 lock(pessimistic lock),而是考慮相似 If-Match 的機制呢?
言歸正傳。以前的整個過程,我都在理解做者的意圖。心滿意足後,我通常會問問:
這代碼有能夠優化的地方麼?
有潛在的安全漏洞麼?
是否有未處理的狀態或者異常?
這短短五行的代碼,lists:member 是個 O(N) 的操做。O(N) 的操做都該引發咱們的注意。咱們知道,membership check 用 set 而非 list,效果更佳。從安全的角度講,split 出來的 ETags 是個薄弱環節。攻擊者能夠構造足夠大且複雜的 if-match 頭,來扯慢單個 request 的處理,從而達到更好的 DoS 效果。至於未處理的狀態,這裏能夠放心 —— 有了上文中所示的如此詳盡的流程圖(狀態機),這代碼不會有大的問題。
OK,這栗子炒的時間夠長了,打住。咱們對比一下三個 API framework 的代碼量:liberator 1.2k,webmachine 5k,eve 12k。讀 liberator 的感受像是楚辭,優美但晦澀;讀 webmachine 的感受像是數學教材,滿紙都是遞歸推導;讀 eve 的感受像是讀本科生的論文,完成了功能而已,讀完沒啥印象,卻是有些反面教材:整個 framework 寫得太死,一些本不應 framework 作的決策被作了,以致於要知足咱們的某些需求,最終只能經過 fork 它改 framework 的代碼來完成,而這是 framework 的大忌(咱們當時用的是 0.4,寫本文時是 0.7)。
咱們總結一下 —— 爲了明理而閱讀代碼的方法並不太難:
先使用前面所述的檢視閱讀法把整個代碼過一遍,找到值得閱讀的核心代碼。
粗讀這部分代碼,將其內容進一步 breakdown。手邊準備好筆和紙(或者其餘趁手的工具),隨時記錄。記錄最好的方式是圖表。這個階段的記錄不建議用軟件工具(除非有用着特別舒服的,可以人件合一的)。
精讀這部分代碼,結合你已有的知識,理解這個代碼所須要的資料,猜想和還原代碼中某種事件,消息,或者某個流程發生的場景。把猜想記錄下來。這時,若是遇到外圍的代碼(調用了外部的函數),只要對理解不產生障礙,能夠先放一下,把整個過程完整而詳細地捋一遍再說。這個過程必定要多問問題,把「我覺得我懂了但實際沒懂」的情形儘量減小。
用檢視閱讀法粗度剩下的代碼,若是找到其餘值得精讀的代碼,跳至 2。
使用對比閱讀(或者說,主題閱讀)方式,把相似功能的 repo 都掃一遍。嘗試着用本身的語言消化不一樣做者的實現,關注其實現的差別,並試圖評判這種差別。
用軟件將手稿電子化,便於未來回顧。文字能夠直接上筆記本工具(甚至能夠嘗試 gitbook),圖表若是買不起 visio,omniGraffle 這樣的工具,能夠用 plantuml。使用方法參考個人文章:那些年,我追過的繪圖工具
最後一步是個很是耗時的過程,除非你有驚人的毅力,或者好爲人師,要把你的心得分享出去,不然,作的動力不大。固然,在這個場景下,咱們是怡然自得地讀代碼,沒有客戶老闆拿着鞭子在後面抽,因此讀懂以後記憶會很是深入,即使沒有電子文檔留存,回到代碼翻一翻,記憶就能還原了。
再次檢討:我第 6 步也作得很差,有些手稿,若是不拍成照片,就永遠的丟失了。
(我對 app master 的初始化的簡單總結)
這樣的閱讀作得越多,真正搞明白的理論,知識和算法越多,你就越是不可替代的大拿。有時候,其實咱們只要認認真真花上幾個月,搞明白某大型項目,基本就是人中龍鳳了。
場景三:爲了能級躍遷而閱讀代碼
中學物理告訴咱們:原子在光的輻射下,吸取光子,能夠從低能狀態跳躍到高能狀態,電子的軌道,或者說能級(energy level)會發生躍遷。這和哲學上經常提到的量變到質變殊途同歸。做爲一個程序員,你的發展歷程也是這麼一個過程:在工做中緩慢爬坡,到達一個平臺便停滯不前,彷佛股票走勢上的箱體整理。而後忽然有一段時間,不知道受了什麼刺激(好比說野戰打出了龍王神力,或者說讀了程序人生*_*),忽然連拉幾個漲停上了另外一個平臺。
打破平臺期,成就能級躍遷,你須要吸取合適的「光子」。這光子能夠是一個開天闢地的項目(好比說 Google 的 Google Map,docker 的 docker,阿里的淘寶等),但是這樣的機會並不是總能被你我遇上,大多數人都是在日復一日地作些並不起眼的,只能緩緩升級的小活 —— 就像一個七級的英雄帶着一羣大雕,卻只能每天懟大耳怪地獄惡犬獅鷲之流的野兵。這時候,與其默默沉淪,不若學莊子口中的北冥之魚那樣,沉潛浮動,積蓄能量,等待下一次摶扶搖而上九萬里。
這種積蓄能量爲躍遷準備的一種方式是讀代碼。讀什麼?讀那些基礎地不能再基礎,你認爲本身一生都不會去寫的那些代碼。好比 linux kernel,好比 OTP。這種讀法,你不知什麼時候可以完成,因此,要有足夠的耐心和時間。檢視閱讀 + 主題閱讀 + 思惟導圖是常常用到的方法。下圖是 OTP 的源碼我在檢視閱讀後,圈定的要逐漸閱讀的部分,加粗的是我已經完成粗讀的部分。
OTP 的代碼不算少,可是耦合度很是低,其實最終是拆分紅若干個場景二去閱讀。咱們看其代碼總量:
1.4m loc,近乎恐怖。但除去 example 和一些無關的輔助代碼後:
930k,小了約 45%。而我圈定的第一批閱讀的代碼,只有區區 130k 而已,不消兩天能夠粗讀,頂多半年,能夠精讀。如此這般,半年後,你的水平必然非當日的吳下阿蒙了。
須要指出的是,這種閱讀有時會讓人很是沮喪,由於你會碰見很是很是之多的 knowledge gap,從而不得不翻書查資料彌補這些你缺失的知識點,拉慢了整個閱讀理解的步伐 —— 有時甚至很多天毫無進展,讓你心中被激發出來的那口氣開始漸漸衰竭。這時候,穩住!這些 knowledge gap 是上天饋贈的禮物,是你彌補 you don't know what you don't know 的絕佳機會。慢慢來,take it easy,享受獲取額外知識的喜悅。
這樣數年下來不斷充實本身,你寫代碼作項目時,離余光中老師描繪的,使人神往的李白的「繡口一吐,就半個盛唐」的狀態不遠矣!
分享一個小故事,結束這篇文章:
在 Juniper 的早年,我在寫 netscreen data plane 代碼的總結(就是我在其餘文章中提到過的被公司同事戲謔爲『葵花寶典』的那分內部文檔)時,由於想更加弄清楚 IPSec phase 1 SA 創建的過程,潛進了 IKE 的後花園。彼時我對 IKEv2 不甚瞭解,讀了不少資料,而後纔開始看代碼。代碼我看得懵懵懂懂,catcher,thrower 等奇奇怪怪的表述讓我如同黃帝陷入了蚩尤的迷霧中不得方向。後來在同事的提醒下,我才知道那一堆術語都源自棒球,在 wikipedia 上搞懂了這些棒球術語的意思後,那些代碼開始變得可愛起來。
幾年之後,我第一次讀了『如何閱讀一本書』,做者用了一大段文字經過棒球的捕手的技巧來類比閱讀的技巧,用捕手和投手的關係來類比讀者和做者的關係,讀者讀者,我不由回到了十多年前那個盡是陽光的午後:我坐在寬敞的辦公桌前,使用桌上的無盤 SunRay 工做站接入了一臺以格林童話 Gretel and Hansel 命名的 solaris 服務器上,而後使用 vim 打開了 IKE 的創建鏈接,六次(不知道記憶是否還對)握手的代碼,咂着香濃的咖啡,開始觀看一場精彩紛呈的棒球比賽。。。