12.2 サイトエラー処理

我々のWebアプリケーションが一旦実運用されると、さまざまなエラーが発生する可能性があります。Webアプリケーションの日常の実行ではいくつものエラーが発生する可能性があります。具体的には以下のとおり:

  • データベースエラー:データベースサーバへのアクセスまたはデータと関係のあるエラーです。例えば、以下は何らかのデータベースエラーを発生させることがあります。

    • 接続エラー:このエラーはデータベースサーバのネットワークが切断された時や、ユーザ名とパスワードが不正だった場合、またはデータベースが存在しない場合に発生することがあります。
    • 検索エラー:使用されたSQLが正しく無く、エラーが発生する場合です。このようなSQLエラーはもしプログラムに厳格なテストを行うことで回避できます。
    • データエラー:データベースの約束が衝突する場合。例えば一つしかないフィールドに複数の主キーを持つデータが挿入されるとエラーを発生させます。しかし、あなたのアプリケーションプログラムが運用される前に厳格なテストを行うことでこれらの問題を回避することもできます。
  • アプリケーション実行時のエラー:これらのエラーの範囲は非常に広く、コードの中で出現するほとんどすべてのエラーをカバーしています。ありえるアプリケーションエラーは以下のような状況です:

    • ファイルシステムとパーミッション:アプリケーションが存在しないファイルを読み込むか、権限の無いファイルを読むか、書き込む事を許されていないファイルに書き込みを行うといったこれらの行為は全てエラーを発生させます。もしアプリケーションが読み込むファイルのフォーマットが正しくなかった場合もエラーを発生させます。例えば設定ファイルがiniの設定フォーマットでなければならないのに、json形式で設定されているとエラーを発生させます。
    • サードパーティアプリケーション:もし我々のアプリケーションプログラムを他のサードパーティのインターフェースプログラムと組み合わせる場合、例えばアプリケーションプログラムがテキストを出力し、自動的にマイクロブログのインターフェースをコールすると、このインターフェースは正常に実行されなければ我々のテキストを出力する機能は実現することができません。
  • HTTPエラー:これらのエラーはユーザのリクエストによって発生するエラーです。もっともよく見かけるのは404エラーです。その他にも多くのエラーが発生することはあるとしても、他によく見かけるエラーには401無許可エラー(認証によってアクセスできるリソース)、403アクセス拒否エラー(ユーザがリソースにアクセスするのを拒否)と503エラー(プログラムの内部エラー)です。

  • オペレーティングシステムエラー:これらのエラーはすべてアプリケーション上のオペレーティングシステムによって発生するものです。主にオペレーティングシステムのリソースが分配されたり、フリーズを引き起こしたり、オペレーティングシステムのハードディスクをいっぱいにして書き込みができなくなったりと、多くのエラーを引き起こします。
  • ネットワークエラー:これは2つのエラーを示します。ひとつはユーザがアプリケーションにリクエストを行う場合ネットワークが切れてしまうもので、ネットワークの接続が中断されてしまいます。これらのエラーはアプリケーションの崩壊こそ招きませんが、ユーザのアクセス効果に影響を及ぼします。もうひとつはアプリケーションプログラムがほかのネットワーク上のデータを読み込み、その他のネットワークが切断することで読み込みに失敗するものです。これらはアプリケーション・プログラムに有効なテストを施すことで、これらの問題が発生する状況でアプリケーションが崩壊することを防ぐことができます。

エラー処理の目標

エラー処理を実装する前に、エラー処理が目指す目標が何かを明確にすべきです。エラー処理システムは以下のような作業のもと行います:

  • アクセスしたユーザにエラーの発生を通知する:発生したのがシステムエラーであれユーザのエラーであれ、ユーザは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("システムエラー")   //システムエラーはクリティカルですので、ログに記録するだけでなくメールを送信します。
          t, _ = t.ParseFiles("tmpl/error.html", nil)  //テンプレートファイルを解析
          ErrorInfo := "システムは現在ご利用できません" //現在のユーザ情報を取得
          t.Execute(w, ErrorInfo)  //テンプレートのmerger操作を実行
      }
    

どのように例外を処理するか

多くの他の言語の中にはtry..catchキーワードがあることをご存知だと思います。例外をキャッチするために使う状況ですが、そもそもエラーの多くはあらかじめ発生が予測できるものばかりで、例外処理を行う必要はありません。エラーによって処理しなければならないのも、Go言語が関数にエラーを返させる設計になっているためです。これはpanicになりません。もしあなたが切れたネットワーク接続に対してデータを書き込む場合、net.ConnシリーズのWrite関数がエラーを返します。これらはpanicになりません。これらの状態はこのようなプログラムにおいて予測できるものです。あなたがこれらの操作が失敗しうると知っているのは、設計者がエラーを返す事で明確にこれを表明しているからです。これが上で述べた発生が予測可能なエラーです。

しかしまた別の状況もあります。ある操作がほとんど失敗せず、ある特定の状況下においてエラーを返すこともできず、継続して実行することもできない場合、panicになります。例をあげましょう:もしあるプログラムがx[j]を計算したところjが範囲を超えてしまった場合、この部分のコードはpanicを引き起こします。このように予測できない重大なエラーがpanicを引き起こします。デフォルトではこれはプロセスを殺します。これは現在実行されているこのコードのgoroutineがエラーを発生させた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ではエラーと例外は常に明確な区別がなされます。そのため、我々がプログラムを設計するにあたってエラーと例外を処理する際はどのような原則に従うべきかについてご紹介しました。

results matching ""

    No results matching ""