代码调试的最佳指南
調(diào)試資源
我希望有更多的關(guān)于代碼調(diào)試的書籍/指南,在這里我有兩個(gè)推薦:David Agans 寫的《Debugging》:有幾個(gè)人向我推薦了這本《Debugging》,它看起來是一本很好的關(guān)于代碼調(diào)試的書,用簡(jiǎn)短的篇幅闡述了一些代碼調(diào)試策略。這本書我還沒有讀過,但是我已經(jīng)買了一本,我希望我讀完后決定是否應(yīng)該推薦它。這本書中闡述的一些代碼調(diào)試應(yīng)該遵循的規(guī)則似乎很有道理,比如說“了解系統(tǒng)”,“讓它失敗”,“別想了,先看看”,“分而治之”,“一次只改變一件事情”,“保持審查詳細(xì)記錄”,“從一個(gè)新的角度看問題”,和“如果你沒有修復(fù)它,它就不會(huì)修復(fù)”等等。另外,這本書還有一張吸引人的的代碼調(diào)試的海報(bào)。John Regehr寫的“How to debug(如何調(diào)試)”:How to Debug是John Regehr基于他自己在大學(xué)里教授嵌入式系統(tǒng)課程的經(jīng)驗(yàn)寫的一篇非常好的博客文章(https://blog.regehr.org/archives/199),里面有很多針對(duì)代碼調(diào)試的好建議。他還發(fā)表了一篇博文(https://blog.regehr.org/archives/849)來評(píng)論4本關(guān)于代碼調(diào)試的書籍,包括了David Agans s寫的這本《Debugging》。重現(xiàn)你的bug(但是要怎么做?)
接下來在這篇文章里,我將嘗試整理大家針對(duì)我的關(guān)于代碼調(diào)試的推文發(fā)來的各種不同的觀點(diǎn)和看法。從這些看法中很明顯地看出,所有人都同意這一點(diǎn):如果你想弄清楚發(fā)生了什么,那么能夠持續(xù)地重現(xiàn)一個(gè)bug非常重要。我對(duì)如何做到這一點(diǎn)有直覺,但是對(duì)于怎樣才能從“我看到這個(gè)bug兩次”跨越到“我可以根據(jù)需要在筆記本電腦上持續(xù)地再現(xiàn)這個(gè)bug”這一點(diǎn),我不知道怎么解釋,而且我想知道你用來調(diào)試的技術(shù)是否依賴于這些不同的開發(fā)領(lǐng)域:后端web開發(fā),前端開發(fā),移動(dòng)開發(fā),游戲開發(fā),C++編程,嵌入式開發(fā)等等。快速重現(xiàn)bug
所有人也都同意,能夠快速地重現(xiàn)bug是非常有用的(如果每次更改都需要3分鐘來檢查是否有幫助,那么迭代就太慢了)。這里有一些建議的方法:對(duì)于那些需要在瀏覽器中進(jìn)行很多次點(diǎn)擊才能重現(xiàn)的bug,用Selenium記錄你點(diǎn)擊的內(nèi)容,并讓Selenium重播UI交互(詳細(xì)的建議請(qǐng)見這里:https://twitter.com/AnnieTheObscure/status/1142843984642899968);
如果你能夠的話,編寫一個(gè)重現(xiàn)錯(cuò)誤的單元測(cè)試。這樣做還有另外一個(gè)好處:如果這個(gè)單元測(cè)試有意義的話,你可以稍后將它添加到測(cè)試套件中;
編寫一個(gè)腳本,或者找到一個(gè)命令行命令幫助你做它(比如curl MY_APP.local/whatever))。
承認(rèn)bug可能是你寫的代碼引起
有時(shí)我看到一個(gè)問題,我會(huì)說“哦,X庫有個(gè)bug”,或者“哦,這是DNS錯(cuò)誤造成的”,或者“哦,不是我的代碼,而是其它地方的錯(cuò)誤造成的”。確實(shí)有時(shí)候一個(gè)bug不是我寫的代碼造成的!但一般來說,在一個(gè)已經(jīng)驗(yàn)證的庫和我上個(gè)月編寫的代碼之間,通常是我上個(gè)月編寫的代碼才是真正的問題所在 。開始實(shí)驗(yàn)
@act_gardnerd在Twitter上給出了一個(gè)很好的簡(jiǎn)短的回答(https://twitter.com/act_gardner/status/1142838587437830144),解釋了你在再現(xiàn)你的bug之后,你需要做什么。原文如下:我試著鼓勵(lì)人們首先對(duì)這個(gè)bug有個(gè)全面的理解,比如說:什么正在發(fā)生?你期望會(huì)發(fā)生什么?什么時(shí)候會(huì)發(fā)生?什么時(shí)候不發(fā)生?然后運(yùn)用他們對(duì)系統(tǒng)的心理模型來猜測(cè)可能發(fā)生的破壞,并進(jìn)行實(shí)驗(yàn)。實(shí)驗(yàn)可以是更改或刪除代碼,從一個(gè)REPL調(diào)用API,嘗試新的輸入,使用調(diào)試器(debugger)或print語句來獲取內(nèi)存中的值。我認(rèn)為這里可能需要循環(huán)地重復(fù)以下步驟:猜測(cè)可能發(fā)生的錯(cuò)誤的某一個(gè)方面(比如說,“這個(gè)變量被設(shè)置為X,它應(yīng)該是Y”,或“發(fā)送到服務(wù)器的請(qǐng)求是錯(cuò)誤的”,或“這段代碼根本沒有運(yùn)行過”等等)。
做實(shí)驗(yàn)來驗(yàn)證這個(gè)猜測(cè)。
重復(fù)循環(huán),直到你明白發(fā)生了根源所在。
檢查你的假設(shè)
很多調(diào)試工作都基于一個(gè)假設(shè):你確定的事情是真的(比如說:“等一下,這個(gè)請(qǐng)求是要發(fā)送到新服務(wù)器,對(duì)吧,不是舊服務(wù)器????)。但是實(shí)際上……不是真的。我試圖列出一些常見的錯(cuò)誤假設(shè)。下面是一些例子:此變量設(shè)置為X(“該文件名絕對(duì)正確”);
該變量的值不可能在X和Y之間變化;
這段代碼以前沒有問題;
此函數(shù)執(zhí)行X;
我正在編輯正確的文件;
我寫的那一行代碼不可能有任何拼寫錯(cuò)誤,只是一行代碼而已;
文檔是正確的;
我正在查看的代碼在某個(gè)時(shí)刻被執(zhí)行;
這兩段代碼是按順序執(zhí)行的,而不是并行執(zhí)行的;
這段代碼在調(diào)試模式和發(fā)布模式下編譯(使用或不使用-O2開關(guān),或…)時(shí),會(huì)做同樣的事情;
編譯器沒有錯(cuò)誤(這是故意放在最后的一個(gè)錯(cuò)誤,很少有人會(huì)認(rèn)為編譯器會(huì)出錯(cuò))。
獲取信息的奇招
有很多正常的方法可以做實(shí)驗(yàn)來檢查你對(duì)代碼所做的假設(shè)/猜測(cè)(比如,打印變量值,使用調(diào)試器,等等)。但是,有時(shí)候你所處的環(huán)境更為困難,你無法打印出內(nèi)容,也無法訪問調(diào)試器(可能是執(zhí)行這些操作不方便,因?yàn)橐幚淼氖录?#xff09;。這里有一些應(yīng)對(duì)方法:在手機(jī)上添加聲音:“在移動(dòng)開發(fā)世界里,這條建議給了我很大幫助。Xcode可以在你遇到斷點(diǎn)時(shí)播放聲音(并且代碼不停止而繼續(xù)執(zhí)行下去)。我把它們放在代碼中的某個(gè)位置,然后聽嗡嗡的叮當(dāng)聲來指示代碼中發(fā)生的錯(cuò)誤”(欲知詳情,請(qǐng)查看上面提到的推文)。
關(guān)于使用Xcode播放iOS代碼調(diào)試的聲音,這里(https://qnoid.com/2013/06/08/Sound-Debugging.html)有一些很有趣的討論。
添加發(fā)光二極管(LED):“很久以前,當(dāng)我們?cè)赥ransputer網(wǎng)格上做嵌入式開發(fā)時(shí),我們將發(fā)光二極管連接到每個(gè)芯片的一個(gè)未使用的管腳上。它在診斷并行性問題上出奇地有效。”
string: “我的網(wǎng)絡(luò)教授告訴我這樣一個(gè)故事,在早期的以太網(wǎng)時(shí)代,他在施樂公司(Xerox)看到了一個(gè)黑客:他使用一個(gè)帶有放大器,馬達(dá)和一根繩子的同軸電纜接頭。網(wǎng)絡(luò)越忙,線就轉(zhuǎn)得越快。”
Peep是一個(gè)“Network Auralizer”,可以將系統(tǒng)上發(fā)生的事情轉(zhuǎn)換成聲音。我花了10分鐘試圖讓它編譯,但迄今為止失敗了,但它看起來很有趣,我想繼續(xù)嘗試它!!
編寫代碼使其更易于調(diào)試
一些人提到的另外一個(gè)觀點(diǎn)是:我們可以改進(jìn)程序,使其更加易于調(diào)試。tef對(duì)此有一篇很好的文章:編寫易于刪除和調(diào)試的代碼(https://programmingisterrible.com/post/173883533613/code-to-debug)。我覺得下面這一點(diǎn)很正確:可調(diào)試的代碼并不一定干凈,而充斥著檢查或錯(cuò)誤處理的代碼很少能讓人愉快地閱讀。我個(gè)人認(rèn)為:“易于調(diào)試”的一種解釋是“每當(dāng)出現(xiàn)錯(cuò)誤時(shí),程序都會(huì)以易于理解的方式向你準(zhǔn)確地報(bào)告發(fā)生的事情”。每當(dāng)我的程序有問題并且報(bào)告這樣的錯(cuò)誤信息“Error:無法連接到某個(gè)IP的端口443:連接超時(shí)”時(shí),我都想說:“謝謝,這就是我想知道的事情”。有了這樣的錯(cuò)誤信息,我就可以檢查我是否需要修復(fù)防火墻,或者我是否由于某種原因得到了錯(cuò)誤的IP地址。最近我碰到一個(gè)簡(jiǎn)單的例子:我向一個(gè)我寫的服務(wù)器發(fā)出請(qǐng)求,得到的回應(yīng)是“upstream connect error or disconnect/reset before headers”。這是一個(gè)nginx錯(cuò)誤,在本例中基本上是因?yàn)椤俺绦蛟陧憫?yīng)一個(gè)請(qǐng)求而發(fā)送任何內(nèi)容之前崩潰了”。找出崩潰的原因是很容易的,但是有更好的錯(cuò)誤處理方式(返回錯(cuò)誤而不是崩潰)可以節(jié)省我一點(diǎn)時(shí)間,因?yàn)槲也槐厝z查崩潰的原因,我只需閱讀錯(cuò)誤信息,知道發(fā)生了什么就可以了。錯(cuò)誤消息好過無提示的程序失敗
為了更接近“每次出現(xiàn)錯(cuò)誤時(shí),程序都會(huì)以一種易于理解的方式向你報(bào)告發(fā)生的事情”的夢(mèng)想,你還需要遵守這條“立即返回錯(cuò)誤消息”的鐵律,而不是默默地向另一個(gè)功能寫入不正確的數(shù)據(jù)或者傳遞無意義的數(shù)據(jù),誰都不知道它會(huì)拿這些數(shù)據(jù)做什么,結(jié)果只會(huì)讓你頭痛。要做到這點(diǎn),意味著你要添加如下代碼:if?UNEXPECTED_THING:raise?"oh?no?THING?happened"獲得正確的錯(cuò)誤信息并不容易,因?yàn)槟阍诔绦虍?dāng)中哪里犯了錯(cuò)誤并不總是顯而易見的,但是這樣做確實(shí)有很大幫助。
failure:返回一堆錯(cuò)誤,而不僅僅是一個(gè)錯(cuò)誤
為了返回更加易于調(diào)試的有用錯(cuò)誤,Rust提供了一個(gè)非常令人難以置信的錯(cuò)誤處理庫failure,它基本于允許你返回一系列錯(cuò)誤,而不僅僅是一個(gè)錯(cuò)誤,因此你可以打印出一堆錯(cuò)誤,如:"error?starting?server?process"?caused?by"error?initializing?logging?backend"?caused?by
"connection?failure:?timeout?connecting?to?1.2.3.4?port?1234".這比僅僅返回connection failure: timeout connecting to 1.2.3.4 port 1234本身要有用得多,因?yàn)樗€告訴你和IP 1.2.3.4有關(guān)的其它一些重要的信息(比如上面這個(gè)錯(cuò)誤就顯示它和日志后端有關(guān)!)。我認(rèn)為它也比返回帶有堆棧跟蹤信息的connection failure: timeout connecting to 1.2.3.4 port 1234的錯(cuò)誤信息更加有用:因?yàn)樗鼘⒍褩8櫺畔⒅械年P(guān)鍵的出錯(cuò)部分總結(jié)出來,這樣你就不需要讀取堆棧跟蹤中的每一行(因?yàn)槠渲幸恍┛赡懿幌嚓P(guān)!).其它語言中的類似于Rust語言failure庫的工具有:
Go語言:它的習(xí)慣用法似乎是把你的一堆錯(cuò)誤串成一個(gè)大字符串,這樣你就得到了一長(zhǎng)串的像這樣的錯(cuò)誤提示:“error:第一個(gè)錯(cuò)誤:error:第二個(gè)錯(cuò)誤:error:第二個(gè)錯(cuò)誤”。它工作得很好,但是它的錯(cuò)誤信息的結(jié)構(gòu)比failure庫能提供的要差得多。
Java語言:我聽說Java可以給出異常的原因(Causes of exceptions), 但是我自己沒有用過。
Python 3:你可以使用raise ... from設(shè)置異常的“__cause__”屬性,然后你的異常將被這句話分開:The above exception was the direct cause of the following exception:..
了解錯(cuò)誤消息的含義
我經(jīng)常理所當(dāng)然地認(rèn)為代碼調(diào)試的一個(gè)子技巧是:正確理解錯(cuò)誤消息的含義!我在這里(https://pythonforbiologists.com/29-common-beginner-errors-on-one-page/)看到了這個(gè)很好的圖形,它解釋了常見的Python錯(cuò)誤以及它們的含義,并且將一些錯(cuò)誤如 NameError, IOError,等等分離開來。我認(rèn)為解釋錯(cuò)誤消息很困難的一個(gè)原因是理解一個(gè)新的錯(cuò)誤消息可能意味著學(xué)習(xí)一個(gè)新的概念。比如,NameError可能代表“你的代碼使用了一個(gè)它定義的變量作用域之外的一個(gè)變量”,但是要真正理解它的意思,你首先得搞清楚什么是變量作用域。我在學(xué)習(xí)Rust的時(shí)候經(jīng)常碰到這樣的問題,Rust編譯器會(huì)提示我“你有一個(gè)奇怪的lifetime錯(cuò)誤”,而我就會(huì)想“呃,好吧,Rust,我知道了,現(xiàn)在我就去搞清楚lifetime是如何工作的!”很多時(shí)候,錯(cuò)誤消息都往往是由一個(gè)與消息文本根本不相干的錯(cuò)誤引起的,比如說“upstream connect error or disconnect/reset before headers”這個(gè)錯(cuò)誤可能意味著“Julia,你的服務(wù)器崩潰了!”當(dāng)你切換到一個(gè)新的開發(fā)領(lǐng)域時(shí),理解錯(cuò)誤消息的技能通常是不可轉(zhuǎn)移的(假如我明天開始大量地編寫React或其它編程語言的代碼,一開始我可能根本不知道任何錯(cuò)誤消息的含義!)。所以這個(gè)問題絕對(duì)不僅僅是初學(xué)者需要面臨的問題。結(jié)語
當(dāng)我在談到代碼調(diào)試技巧時(shí),我總感覺我遺漏了一件重要的事情,那就是對(duì)人們?cè)诖a調(diào)試中哪里會(huì)遇到困難的一種更深入的理解。通常我們很容易說:“好吧,你需要重現(xiàn)這個(gè)問題。那么先讓我們進(jìn)行最小化的重現(xiàn),你可以開始猜測(cè)和驗(yàn)證你的猜測(cè),改進(jìn)你對(duì)系統(tǒng)的思維模式,找出問題所在,然后解決問題。最后寫一個(gè)測(cè)試,希望它不再重現(xiàn)”,但是,實(shí)際上,我們很難確定人們到底會(huì)在哪里遇到困難和最難的部分是什么。對(duì)我自己而言代碼調(diào)試最難的地方是什么,我通常會(huì)有點(diǎn)思路。但是對(duì)那些新人而言,代碼調(diào)試最難的地方是什么,我依然是云里霧里,毫無頭緒。原文:https://jvns.ca/blog/2019/06/23/a-few-debugging-resources/————
編輯?∑Gemini
?來源:程序員的那些事
?
?
?
?
?
?
?
?
?
?
?
?
?
算法數(shù)學(xué)之美微信公眾號(hào)歡迎賜稿
稿件涉及數(shù)學(xué)、物理、算法、計(jì)算機(jī)、編程等相關(guān)領(lǐng)域,經(jīng)采用我們將奉上稿酬。
投稿郵箱:math_alg@163.com
總結(jié)
- 上一篇: 屠呦呦入选《时代周刊》100位最具影响力
- 下一篇: 施一公:带好学生,是特别要紧的事