網站錯誤處理

我們的 Web 應用一旦上線之後,那麼各種錯誤出現的概率都有,Web 應用日常執行中可能出現多種錯誤,具體如下所示:

  • 資料庫錯誤:指與存取資料庫伺服器或資料相關的錯誤。例如,以下可能出現的一些資料庫錯誤。

    • 連線錯誤:這一類別錯誤可能是資料庫伺服器網路斷開、使用者名稱密碼不正確、或者資料庫不存在。

    • 查詢錯誤:使用的 SQL 非法導致錯誤,這樣子 SQL 錯誤如果程式經過嚴格的測試應該可以避免。

    • 資料錯誤:資料庫中的約束衝突,例如一個唯一欄位中插入一條重複主鍵的值就會報錯,但是如果你的應用程式在上線之前經過了嚴格的測試也是可以避免這類別問題。

  • 應用執行時錯誤:這類別錯誤範圍很廣,涵蓋了程式碼中出現的幾乎所有錯誤。可能的應用錯誤的情況如下:

    • 檔案系統和許可權:應用讀取不存在的檔案,或者讀取沒有許可權的檔案、或者寫入一個不允許寫入的檔案,這些都會導致一個錯誤。應用讀取的檔案如果格式不正確也會報錯,例如配置檔案應該是 ini 的配置格式,而設定成了 json 格式就會報錯。

    • 第三方應用:如果我們的應用程式耦合了其他第三方介面程式,例如應用程式發表文章之後自動呼叫接發微博的介面,所以這個介面必須正常執行才能完成我們發表一篇文章的功能。

  • HTTP 錯誤:這些錯誤是根據使用者的請求出現的錯誤,最常見的就是 404 錯誤。雖然可能會出現很多不同的錯誤,但其中比較常見的錯誤還有 401 未授權錯誤(需要認證才能存取的資源)、403 禁止錯誤(不允許使用者存取的資源)和 503 錯誤(程式內部出錯)。

  • 作業系統出錯:這類別錯誤都是由於應用程式上的作業系統出現錯誤引起的,主要有作業系統的資源被分配完了,導致宕機,還有作業系統的磁碟滿了,導致無法寫入,這樣就會引起很多錯誤。

  • 網路出錯:指兩方面的錯誤,一方面是使用者請求應用程式的時候出現網路斷開,這樣就導致連線中斷,這種錯誤不會造成應用程式的崩潰,但是會影響使用者存取的效果;另一方面是應用程式讀取其他網路上的資料,其他網路斷開會導致讀取失敗,這種需要對應用程式做有效的測試,能夠避免這類別問題出現的情況下程式崩潰。

錯誤處理的目標

在實現錯誤處理之前,我們必須明確錯誤處理想要達到的目標是什麼,錯誤處理系統應該完成以下工作:

  • 通知存取使用者出現錯誤了:不論出現的是一個系統錯誤還是使用者錯誤,使用者都應當知道 Web 應用出了問題,使用者的這次請求無法正確的完成了。例如,對於使用者的錯誤請求,我們顯示一個統一的錯誤頁面(404.html)。出現系統錯誤時,我們透過自訂的錯誤頁面顯示系統暫時不可用之類別的錯誤頁面(error.html)。

  • 記錄錯誤:系統出現錯誤,一般就是我們呼叫函式的時候回傳 err 不為 nil 的情況,可以使用前面小節介紹的日誌系統記錄到日誌檔案。如果是一些致命錯誤,則透過郵件通知系統管理員。一般 404 之類別的錯誤不需要傳送郵件,只需要記錄到日誌系統。

  • 回復 (Rollback)當前的請求操作:如果一個使用者請求過程中出現了一個伺服器錯誤,那麼已完成的操作需要回復 (Rollback)。下面來看一個例子:一個系統將使用者提交的表單儲存到資料庫,並將這個資料提交到一個第三方伺服器,但是第三方伺服器掛了,這就導致一個錯誤,那麼先前儲存到資料庫的表單資料應該刪除(應告知無效),而且應該通知使用者系統出現錯誤了。

  • 保證現有程式可執行可服務:我們知道沒有人能保證程式一定能夠一直正常的執行著,萬一哪一天程式崩潰了,那麼我們就需要記錄錯誤,然後立刻讓程式重新執行起來,讓程式繼續提供服務,然後再通知系統管理員,透過日誌等找出問題。

如何處理錯誤

錯誤處理其實我們已經在十一章第一小節裡面有過介紹如何設計錯誤處理,這裡我們再從一個例子詳細的講解一下,如何來處理不同的錯誤:

  • 通知使用者出現錯誤:

    通知使用者在存取頁面的時候我們可以有兩種錯誤:404.html 和 error.html,下面分別顯示了錯誤頁面的原始碼:

    <html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>找不到頁面</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">

    </head>
    <body>
    <div class="container">
        <div class="row">
            <div class="span10">
                <div class="hero-unit">
                    <h1>404!</h1>
                    <p>{{.ErrorInfo}}</p>
                </div>
            </div><!--/span-->
        </div>
    </div>
    </body>
    </html>
另一個原始碼:
    <html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>系統錯誤頁面</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">

    </head>
    <body>
    <div class="container">
        <div class="row">
            <div class="span10">
                <div class="hero-unit">
                    <h1>系統暫時不可用!</h1>
                    <p>{{.ErrorInfo}}</p>
                </div>
            </div><!--/span-->
        </div>
    </div>
    </body>
    </html>
404 的錯誤處理邏輯,如果是系統的錯誤也是類似的操作,同時我們看到在:
    func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/" {
            sayhelloName(w, r)
            return
        }
        NotFound404(w, r)
        return
    }

    func NotFound404(w http.ResponseWriter, r *http.Request) {
        log.Error("頁面找不到")   //記錄錯誤日誌
        t, _ = t.ParseFiles("tmpl/404.html", nil)  //解析範本檔案
        ErrorInfo := "檔案找不到" //取得當前報錯資訊
        t.Execute(w, ErrorInfo)  //執行範本的 merger 操作
    }

    func SystemError(w http.ResponseWriter, r *http.Request) {
        log.Critical("系統錯誤")   //系統錯誤觸發了 Critical,那麼不僅會記錄日誌還會發送郵件
        t, _ = t.ParseFiles("tmpl/error.html", nil)  //解析範本檔案
        ErrorInfo := "系統暫時不可用" //取得當前報錯資訊
        t.Execute(w, ErrorInfo)  //執行範本的 merger 操作
    }

如何處理異常

我們知道在很多其他語言中有 try...catch 關鍵詞,用來捕獲異常情況,但是其實很多錯誤都是可以預期發生的,而不需要異常處理,應該當做錯誤來處理,這也是為什麼 Go 語言採用了函式回傳錯誤的設計,這些函式不會 panic,例如如果一個檔案找不到,os.Open 回傳一個錯誤,它不會 panic;如果你向一箇中斷的網路連線寫資料,net.Conn 系列型別的 Write 函式回傳一個錯誤,它們不會 panic。這些狀態在這樣的程式裡都是可以預期的。你知道這些操作可能會失敗,因為設計者已經用回傳錯誤清楚地表明了這一點。這就是上面所講的可以預期發生的錯誤。

但是還有一種情況,有一些操作幾乎不可能失敗,而且在一些特定的情況下也沒有辦法回傳錯誤,也無法繼續執行,這樣情況就應該 panic。舉個例子:如果一個程式計算 x[j],但是 j 越界了,這部分程式碼就會導致 panic,像這樣的一個不可預期嚴重錯誤就會引起 panic,在預設情況下它會殺掉程序,它允許一個正在執行這部分程式碼的 goroutine 從發生錯誤的 panic 中恢復執行,發生 panic 之後,這部分程式碼後面的函式和程式碼都不會繼續執行,這是 Go 特意這樣設計的,因為要區別於錯誤和異常,panic 其實就是異常處理。如下程式碼,我們期望透過 uid 來取得 User 中的 username 資訊,但是如果 uid 越界了就會丟擲異常,這個時候如果我們沒有 recover 機制,程序就會被殺死,從而導致程式不可服務。因此為了程式的健壯性,在一些地方需要建立 recover 機制。

func GetUser(uid int) (username string) {
    defer func() {
        if x := recover(); x != nil {
            username = ""
        }
    }()

    username = User[uid]
    return
}

上面介紹了錯誤和異常的區別,那麼我們在開發程式的時候如何來設計呢?規則很簡單:如果你定義的函式有可能失敗,它就應該回傳一個錯誤。當我呼叫其他 package 的函式時,如果這個函式實現的很好,我不需要擔心它會 panic,除非有真正的異常情況發生,即使那樣也不應該是我去處理它。而 panic 和 recover 是針對自己開發 package 裡面實現的邏輯,針對一些特殊情況來設計。

小結

本小節總結了當我們的 Web 應用部署之後如何處理各種錯誤:網路錯誤、資料庫錯誤、作業系統錯誤等,當錯誤發生時,我們的程式如何來正確處理:顯示友好的出錯介面、回復 (Rollback)操作、記錄日誌、通知管理員等操作,最後介紹了如何來正確處理錯誤和異常。一般的程式中錯誤和異常很容易混淆的,但是在 Go 中錯誤和異常是有明顯的區分,所以告訴我們在程式設計中處理錯誤和異常應該遵循怎麼樣的原則。

Last updated