天天看點

opcdaclient 對com元件的調用傳回了錯誤hresult_講講錯誤處理

opcdaclient 對com元件的調用傳回了錯誤hresult_講講錯誤處理
  • 1. 引言
  • 2. 錯誤/異常
    • 2.1. 異常安全
    • 2.2. 異常中立 exception neutrality
  • 3. C/C++方式
  • 4. 進入node.js
  • 5. 進入go
  • 6. 總結

作為程式員要虛心 ——魯迅

這裡寫的是自己對于錯誤處理的一些了解,末尾列出了參考文章,如果有侵權,可以聯系我修改。如果有寫得不對的地方,請重拍!

1. 引言

錯誤處理是一個曆史很悠久的話題了,其中也有很多相關的文獻,很多大牛也針對這個話題頻出奇招。作為一個軟體開發者,也應該對其中不同的處理模式和背後的思想有些許了解。

2. 錯誤/異常

那什麼是錯誤呢?對于軟體代碼而言,調用方違反被調用函數的precondition、或一個函數無法維持其理應維持的invariants、或一個函數無法滿足它所調用的其它函數的precondition、或一個函數無法保證其退出時的postcondition;以上所有情況都屬于錯誤。

wikipedia把工程學中的錯誤定義為:系統或對象中,預期中的結果和實際的結果之間的差異。Error

而對于異常,除了“錯誤”包含的屬性外,wikipedia中還提到了“異常”的“

終止語義

”Termination semantics

我了解中,“異常”是比“錯誤”更嚴重的錯誤,或者說異常是錯誤的子集。發生異常時會終止目前處理的流程,例如終止目前請求、目前事務,甚至終止服務。大部分情況下,錯誤和異常在含義上相同。但有時候錯誤和異常的分界線并不清楚,而是需要根據不同的場景來定義的。例如打開配置檔案的操作,如果找不到檔案,對于某些場景,需要抛異常來提示配置檔案不存在(流程被終止了);而對有些流程,隻需要加載預設的配置即可,流程還是能繼續走下去(這裡相當于對錯誤做了降級處理)。

為什麼會發生錯誤呢?或者說能不能把代碼寫得盡量完美,那樣是不是就沒有讨論這個話題的必要了?首先,一個方案難免會存在邏輯思維漏洞,或者考慮不周的情況,這時候錯誤也應該是預期之中的,是以我們有“疊代”的概念。另外,現在的工程代碼,不再是幾個人就能手撸出來的,要依賴了大量的外部元件、外部子產品,外部服務等。種種的外部依賴也表示就算你的代碼沒問題,代碼也不會一直遵循happy path走到底。

是以在現實中,如果服務出現可恢複的錯誤時,盡快恢複,不影響到服務繼續運作;當不可恢複時,應做好妥善的後置操作,例如釋放資源、保護使用者資料、記錄錯誤資訊等,必要時重新開機服務。

2.1. 異常安全

boost庫對于通用元件的異常安全的非正式定義是:子產品中的異常安全意味着,當元件在執行過程中抛異常時,它會表現出合理的行為。對于大部分人,“合理”一詞包括所有對錯誤處理的常見預期:資源不能洩漏;程式應該保持在一個明确定義的狀态是以能繼續執行。對于大多數元件,當錯誤發生時,應該讓調用方知曉。

wikipedia定義稍微學術些:

異常安全,exception safety是,類庫實作者和使用者在任何帶有異常的程式設計語言上,可以用來推導異常處理安全性的一系列的協定指導。其中包含4個等級(從強到弱):錯誤透明、強異常安全(事務語義)、基本異常安全、無異常安全。

2.2. 異常中立 exception neutrality

異常應該被特定的

try catch

塊捕獲并處理,并且允許未被捕獲的異常繼續向上傳遞。如果存在一個終止的

catch(...)

,則需要最終把目前的異常

re-throw出去。簡單而言,除非異常被捕獲,不然異常對象應該保持不變,一直傳上去。

3. C/C++方式

C語言的函數沒法擁有多個傳回值。caller想要得到函數執行的結果,又想拿到函數執行過程中可能引發的錯誤。這時候就有點讓人頭大了。是以C提供了一種方式來解決這一類問題,使用

整數狀态碼(内部庫實作用errno)

+

指針

的方式。

  • 狀态碼用來訓示該函數執行是否成功,errno是線程安全的;
  • 指針提供了一種可以将函數改動後資料傳遞給函數外的方式;

C語言使用了狀态碼(status code)模式,但傳回的狀态碼不為0并不代表發生了錯誤,而是不同的接口自己定義的。舉個例子:

// 除法
int division(int dividend, int divisor, float* quotient) {
   if( divisor == 0){
      exit(-1);
   }
   // process divide
   exit(0);
}

// 查找子字元串
int index = find(str, sub_str)
if(index != -1) {
    … // case 1
} else {
    … // case 2
}
           

上面的例子中,除法遇到除數為0時,傳回了-1的錯誤碼,而caller需要根錯誤碼映射關系才能知道錯誤的含義。而在查找的例子中,傳回的狀态碼隻表示找不到對應的子字元串,并不能代表程式發生了錯誤。這其實讓C函數的傳回含義嚴重依賴于文檔說明,沒有文檔的話,除了閱讀源碼,不然你哪知道傳回的-1是個啥意思。另外,無法簡單得到函數的調用棧(當然你也可以說逐層的狀态碼檢查其實不需要調用棧)。

到C++時,增加了對異常的支援,

try-catch

文法。代碼可以寫成

try {
    int code = operation();
    switch(code) {
        case case1:
        //...
        break;
        case case2:
        //...
        break;
        default:
        //...
        break;
} catch(...) {
    // exception handling
}
           

這樣其實就把錯誤和異常給區分開了,對于錯誤,直接通過錯誤碼來傳回;而對于異常,通過

throw Error

的方式讓外層

catch

住來處理,異常可以在調用棧的任意一層處理,如果不處理該異常,異常會一直往上冒泡。C++用RAII技術來保證異常安全,對象在析構的時候自己處理資源的釋放等。C++的異常對比C的模式,能承載更多資訊,包括錯誤資訊,錯誤調用棧等。

4. 進入node.js

node.js的錯誤處理最佳實踐可以參考 Joyent的線上實踐,下面的文字很多都是參考自該文章。這裡之是以加上nodejs的錯誤處理,是我覺得一方面javascript是動态語言;另一方面,nodejs推崇異步操作。

Joyent将錯誤主要分為以下兩大類:

  • 操作性錯誤 operational errors(以下簡稱OE):代表正确的代碼在運作時觸發的錯誤。代碼裡面沒有bug,問題出現在其他地方:系統本身(OOM,打開太多檔案),網絡(請求逾時,socket挂起)。
  • 程式員錯誤 programmer errors(以下簡稱PE):程式裡面的bug。例如:少傳遞參數,參數類型不比對(靜态語言中,有類型檢查和編譯期,可以攔住其中的多種情況)。出于服務可用性考慮,從PE中恢複的最好方式就是立即崩潰,并重新啟動。

node.js中有多種錯誤處理模式,包括

  • try-catch-finally
  • callback(err, ...): 這種callback的錯誤傳遞模式看起來很像go語言的模式。
  • promise reject
  • event emit

通常會将後面三種歸結為

異步的錯誤傳遞

,而第一種稱為

同步的錯誤傳遞

。對于一段代碼,可能會傳遞同步的錯誤(通過throw),也可能會傳遞異步的錯誤(通過傳遞到callback中、通過EventEmitter來觸發),但不應該兩種都使用。

上訴4種錯誤處理模式中,

try-catch-finally

是同步代碼中最常見的處理模式,寫法類似于C++。而在異步代碼中,callback是

最基本

的處理模式,但太多callback會引起callback hell的問題,promise的出現解決了這問題,是以promise reject是異步代碼中

最常見

的處理方式。ES6([email protected])中增加了

async/await

文法,讓異步代碼寫起來和同步代碼一樣,同時也能在異步代碼(僅限于

async/await

的異步代碼)中加

try-catch-finally

來捕獲異常了。event emit則在複雜的情況下才會派上用場,例如某些操作會産生多種結果或者多種錯誤、又或者操作存在多種狀态。(值得注意的是,

try-catch

是無法在callback和event emit中捕獲到throw出來的異常的)

5. 進入go

go對于錯誤處理的态度算是一股清流,真正區分開了錯誤和異常的處理。go語言的函數多值傳回機制也為傳回error提供了便利。而對于異常,未實作主流的

try-catch-finally

,采用了

defer-panic-recover

的文法。

We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. It also tends to encourage programmers to label too many ordinary errors, such as failing to open a file, as exceptional.

In Go, error handling is important. The language's design and conventions encourage you to explicitly check for errors where they occur (as distinct from the convention in other languages of throwing exceptions and sometimes catching them).

go實作者認為将異常和控制結構耦合在一起,代碼會變得複雜。并且好像在鼓勵程式員将普通錯誤也辨別成異常。go語言的設計鼓勵程式員明确的處理每一個出現的錯誤。

The Go paradigm is to check for errors explicitly. A program should only panic if the circumstances under which it panics do not happen during ordinary program executing.

因為不想程式員濫用panic,go區分了error和exception(panic)的使用場景。在go社群之中,普遍認為盡可能不用panic。panic預示着這是個fatal的錯誤,程式應當立馬終止。當使用panic時,你應當假設caller無法處理該問題,并且你的代碼,或是內建了你代碼的程式無法繼續進行下去。

我了解中,

  • go的設計想區分開錯誤處理和控制流的跳轉,在大量使用

    try-catch

    的語言中,代碼并沒有逐層去catch住異常,程式員随心所欲地在某個地方加上

    try-catch

    ,這樣便導緻程式的控制流變得很複雜。
  • go真正賦予了異常”終止語義“。

當然,也有人覺得go的這種設計好像回退到C的時代,

rr := doStuff1()
if err != nil {
    //handle error...
}

err = doStuff2()
if err != nil {
    //handle error...
}

err = doStuff3()
if err != nil {
    //handle error...
}
           

上面代碼看起來确實很像C的status code的方式,隻不過将errno換成了error對象。但Go作者之一的Russ Cox覺得選擇傳回值的錯誤處理方式适用于大型項目,而

try-catch

的模式适合小程式。

6. 總結

以上寫的隻是我接觸過的語言對錯誤處理的不同模式,但在我看來,隻是不同語言推崇的處理模式(或者說程式設計習慣)不同,而在語言的機制上,給了相應的便利,但并不代表A語言沒法采用B語言的錯誤處理模式。更多的是,透過不同語言的錯誤處理方式,能窺到作者對于這個語言的期望和設計思想。是以,還是一句老話,沒有最好的技術,隻有最适合場景的技術。

參考:

  • Lessons Learned from Specifying Exception-Safety for the C++ Standard Library
  • Exception safety
  • What is meant by Resource Acquisition is Initialization (RAII)?
  • Why Go gets exceptions right
  • 錯誤處理(Error-Handling):為何、何時、如何(rev#2)
  • Golang error 的突圍
  • Exceptions
  • go exception pattern
  • Error handling and Go
  • Catching panics in Golang
  • Error Handling in Node.js