Files
build-web-application-with-…/zh-tw/12.2.md
2019-02-26 01:40:54 +08:00

9.7 KiB
Raw Blame History

12.2 網站錯誤處理

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

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

    • 連線錯誤:這一類別錯誤可能是資料庫伺服器網路斷開、使用者名稱密碼不正確、或者資料庫不存在。
    • 查詢錯誤使用的SQL非法導致錯誤這樣子SQL錯誤如果程式經過嚴格的測試應該可以避免。
    • 資料錯誤:資料庫中的約束衝突,例如一個唯一欄位中插入一條重複主鍵的值就會報錯,但是如果你的應用程式在上線之前經過了嚴格的測試也是可以避免這類別問題。
  • 應用執行時錯誤:這類別錯誤範圍很廣,涵蓋了程式碼中出現的幾乎所有錯誤。可能的應用錯誤的情況如下:

    • 檔案系統和許可權應用讀取不存在的檔案或者讀取沒有許可權的檔案、或者寫入一個不允許寫入的檔案這些都會導致一個錯誤。應用讀取的檔案如果格式不正確也會報錯例如配置檔案應該是ini的配置格式而設定成了json格式就會報錯。
    • 第三方應用:如果我們的應用程式耦合了其他第三方介面程式,例如應用程式發表文章之後自動呼叫接發微博的介面,所以這個介面必須正常執行才能完成我們發表一篇文章的功能。
  • HTTP錯誤這些錯誤是根據使用者的請求出現的錯誤最常見的就是404錯誤。雖然可能會出現很多不同的錯誤但其中比較常見的錯誤還有401未授權錯誤(需要認證才能訪問的資源)、403禁止錯誤(不允許使用者訪問的資源)和503錯誤(程式內部出錯)。

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

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

錯誤處理的目標

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

  • 通知訪問使用者出現錯誤了不論出現的是一個系統錯誤還是使用者錯誤使用者都應當知道Web應用出了問題使用者的這次請求無法正確的完成了。例如對於使用者的錯誤請求我們顯示一個統一的錯誤頁面(404.html)。出現系統錯誤時,我們透過自訂的錯誤頁面顯示系統暫時不可用之類別的錯誤頁面(error.html)。
  • 記錄錯誤系統出現錯誤一般就是我們呼叫函式的時候返回err不為nil的情況可以使用前面小節介紹的日誌系統記錄到日誌檔案。如果是一些致命錯誤則透過郵件通知系統管理員。一般404之類別的錯誤不需要傳送郵件只需要記錄到日誌系統。
  • 回滾當前的請求操作:如果一個使用者請求過程中出現了一個伺服器錯誤,那麼已完成的操作需要回滾。下面來看一個例子:一個系統將使用者遞交的表單儲存到資料庫,並將這個資料遞交到一個第三方伺服器,但是第三方伺服器掛了,這就導致一個錯誤,那麼先前儲存到資料庫的表單資料應該刪除(應告知無效),而且應該通知使用者系統出現錯誤了。
  • 保證現有程式可執行可服務:我們知道沒有人能保證程式一定能夠一直正常的執行著,萬一哪一天程式崩潰了,那麼我們就需要記錄錯誤,然後立刻讓程式重新執行起來,讓程式繼續提供服務,然後再通知系統管理員,透過日誌等找出問題。

如何處理錯誤

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

  • 通知使用者出現錯誤:

    通知使用者在訪問頁面的時候我們可以有兩種錯誤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應用部署之後如何處理各種錯誤網路錯誤、資料庫錯誤、作業系統錯誤等當錯誤發生時我們的程式如何來正確處理顯示友好的出錯介面、回滾操作、記錄日誌、通知管理員等操作最後介紹瞭如何來正確處理錯誤和異常。一般的程式中錯誤和異常很容易混淆的但是在Go中錯誤和異常是有明顯的區分所以告訴我們在程式設計中處理錯誤和異常應該遵循怎麼樣的原則。