Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

GoによるWeb開発入門:ミドルウェア、テンプレート、JSON、データベースの基礎

このシリーズの前回の記事では、Goのnet/httpパッケージと、それを本番環境向けのWebアプリケーションに活用する方法について詳しく解説しました。主にルーティングの仕組みと、http.ServeMux型のさまざまな特徴や機能に焦点を当てました。

本記事ではServeMuxに関する議論を締めくくるべく、標準のルーターでミドルウェア関数を実装する方法をデモするとともに、GoでWebサービスを開発する際に必ず役立つその他の標準ライブラリパッケージを紹介します。

Goにおけるミドルウェア

多くの(あるいはすべての)HTTPリクエストに対して共通の処理を実行できるよう機能を設定する手法は、ミドルウェアと呼ばれます。認証、ロギング、Cookie検証などの操作は、ミドルウェア関数として実装されることが多く、通常のルートハンドラの前後でリクエストに対して独立して作用します。

Goでミドルウェアを実装するには、http.Handlerインターフェースを満たす型を用意する必要があります。通常、これはシグネチャがServeHTTP(http.ResponseWriter, *http.Request)であるメソッドをその型に紐付けることを意味します。このメソッドさえ持っていれば、どのような型でもhttp.Handlerインターフェースを満たせます。

簡単な例を見てみましょう。

package main

import "net/http"

type helloHandler struct {
    name string
}

func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello " + h.name))
}

func main() {
    mux := http.NewServeMux()

    helloJohn := helloHandler{name: "John"}
    mux.Handle("/john", helloJohn)
    http.ListenAndServe(":8080", mux)
}

/johnルートに送られたリクエストは、そのままhelloHandler.ServeHTTPメソッドへ渡されます。サーバーを起動し、https://localhost:8080/john にアクセスすれば動作を確認できます。

http.Handlerを実装したいときに毎回カスタム型へServeHTTPメソッドを追加するのはかなり面倒です。そこでnet/httpパッケージは、通常の関数をHTTPハンドラとして使えるhttp.HandlerFunc型を提供しています。

必要なのは、関数が次のシグネチャを持つことだけです。func(http.ResponseWriter, *http.Request)。そしてそれをhttp.HandlerFunc型へ変換します。

package main

import "net/http"

func helloJohnHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello John"))
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/john", http.HandlerFunc(helloJohnHandler))
    http.ListenAndServe(":8080", mux)
}

さらに、上記main関数内のmux.Handle行をmux.HandleFuncに置き換えて、関数を直接渡すこともできます。前回の記事ではこのパターンだけを使っていました。

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/john", helloJohnHandler)
    http.ListenAndServe(":8080", mux)
}

ただしこの時点では名前が文字列にハードコードされており、以前のようにmain関数側からハンドラ呼び出し前に名前を設定することができません。この制限を取り除くには、ハンドラのロジックをクロージャに閉じ込める方法があります。

package main

import "net/http"

func helloHandler(name string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello " + name))
    })
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/john", helloHandler("John"))
    http.ListenAndServe(":8080", mux)
}

helloHandler関数自体はhttp.Handlerインターフェースを満たしませんが、満たす匿名関数を生成して返します。この関数はname引数をクロージャとしてキャプチャしており、呼び出されるたびにアクセスできます。これにより、helloHandler関数は必要なだけ異なる名前で再利用可能になります。

では、これがミドルウェアとどう関係するのでしょうか。実は、ミドルウェア関数の作成は上記とまったく同じやり方で行えます。例のように文字列をクロージャへ渡す代わりに、チェーン内の次のハンドラを引数として渡せばよいのです。

完全なパターンは以下の通りです。

func middleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // ここにミドルウェアのロジックを書く...
    next.ServeHTTP(w, r)
  })
}

上記のmiddleware関数はハンドラを受け取り、ハンドラを返します。匿名関数をhttp.HandlerFunc型へキャストすることで、http.Handlerインターフェースを満たさせられる点にも注目してください。匿名関数の末尾ではServeHTTP()メソッドを呼び出し、制御をnextハンドラへ引き渡しています。認証済みユーザーのIDなど、ハンドラ間で値を受け渡したい場合は、Go 1.7で導入されたhttp.Request.Context()メソッドを使います。

このパターンを示す簡単なミドルウェア関数を書いてみましょう。この関数はリクエストオブジェクトにrequestTimeというプロパティを追加し、helloHandlerがそれを使ってリクエストのタイムスタンプを表示します。

package main

import (
    "context"
    "net/http"
    "time"
)

func requestTime(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        ctx = context.WithValue(ctx, "requestTime", time.Now().Format(time.RFC3339))
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

func helloHandler(name string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        responseText := "<h1>Hello " + name + "</h1>"

        if requestTime := r.Context().Value("requestTime"); requestTime != nil {
            if str, ok := requestTime.(string); ok {
                responseText = responseText + "\n<small>Generated at: " + str + "</small>"
            }
        }
        w.Write([]byte(responseText))
    })
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/john", requestTime(helloHandler("John")))
    http.ListenAndServe(":8080", mux)
}

ミドルウェア関数はhttp.Handler型を受け取って返すため、無限に続く入れ子構造のミドルウェアチェーンを作ることができます。

たとえば以下のように書けます。

mux := http.NewServeMux()
mux.Handle("/", middleware1(middleware2(appHandler)))

Aliceのようなライブラリを使えば、上記の構造をより読みやすい形へ変換できます。

alice.New(middleware1, middleware2).Then(appHandler)

テンプレート処理

シングルページアプリケーションの登場によりテンプレートの利用は減少傾向にありますが、完全なWeb開発ソリューションにおいて依然として重要な要素です。

Goにはテンプレート処理のためのパッケージが2つ用意されています。text/templatehtml/templateです。両者は同じインターフェースを持ちますが、後者はコードインジェクション攻撃への対策として、裏側でエンコード処理を行います。

Goのテンプレートは最も表現力豊かなものではありませんが、十分に目的を果たせますし、本番アプリケーションでも使用できます。実際、人気の静的サイトジェネレーターHugoも、テンプレートシステムの基盤としてGoテンプレートを採用しています。

html/templateパッケージを使って、WebリクエストへのレスポンスとしてHTMLを出力する方法を簡単に見てみましょう。

テンプレートの作成

main.goと同じディレクトリにindex.htmlファイルを作成し、次のコードを追加してください。

<ul>
  {{ range .TodoItems }}
  <li>{{ . }}</li>
  {{ end }}
</ul>

次に、main.goファイルに以下のコードを追加します。

package main

import (
    "html/template"
    "log"
    "os"
)

func main() {
    t, err := template.ParseFiles("index.html")
    if err != nil {
        log.Fatal(err)
    }

    todos := []string{"Watch TV", "Do homework", "Play games", "Read"}

    err = t.Execute(os.Stdout, todos)
    if err != nil {
        log.Fatal(err)
    }
}

go run main.goでプログラムを実行すると、次の出力が得られます。

<ul>
  <li>Watch TV</li>
  <li>Do homework</li>
  <li>Play games</li>
  <li>Read</li>
</ul>

おめでとうございます!最初のGoテンプレートが完成しました。テンプレートファイルで使用した構文を簡単に説明します。

  • Goでは二重波括弧({{}})を使って、テンプレート内でのデータ評価や制御構造(アクションと呼ばれる)を区切ります。
  • rangeアクションにより、スライスなどのデータ構造を反復処理できます。
  • .は現在のコンテキストを表します。rangeアクション内での現在のコンテキストはtodosスライスであり、ブロック内の{{ . }}はスライスの各要素を指します。

main.goファイルでは、template.ParseFilesメソッドを使って1つ以上のファイルから新しいテンプレートを作成しています。作成されたテンプレートは、続いてtemplate.Executeメソッドで実行されます。このメソッドはio.Writerと、テンプレートへ適用するデータを受け取ります。

上記の例では標準出力へ実行しましたが、io.Writerインターフェースを満たすものであれば任意の出力先へ実行できます。たとえばWebリクエストのレスポンスとして結果を返したい場合は、以下のようにResponseWriterインターフェースへテンプレートを実行するだけで済みます。

package main

import (
    "html/template"
    "log"
    "net/http"
)

func main() {
    t, err := template.ParseFiles("index.html")
    if err != nil {
        log.Fatal(err)
    }

    todos := []string{"Watch TV", "Do homework", "Play games", "Read"}

    http.HandleFunc("/todos", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "text/html")
        err = t.Execute(w, todos)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    })
    http.ListenAndServe(":8080", nil)
}

ここではGoのテンプレートパッケージの概要を紹介しただけです。より複雑なユースケースに関心がある場合は、text/templateおよびhtml/templateの公式ドキュメントを必ず確認してください。

Goのテンプレート方式が好みでない場合も、Plushライブラリなど代替手段が存在します。

JSONの扱い方

JSONオブジェクトを扱う必要があるなら朗報です。Goの標準ライブラリには、JSONの解析とエンコードに必要なすべてがencoding/jsonパッケージとして含まれています。

デフォルトの型マッピング

GoでJSONオブジェクトをエンコードまたはデコードする際には、以下の型が使用されます。

  • JSONブーリアン → bool
  • JSON数値 → float64
  • JSON文字列 → string
  • JSON null → nil
  • JSONオブジェクト → map[string]interface{}
  • JSON配列 → []interface{}

エンコード

データ構造をJSONへエンコードするには、json.Marshal関数を使用します。例を挙げます。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    FirstName string
    LastName  string
    Age       int
    email     string
}

func main() {
    p := Person{
        FirstName: "Abraham",
        LastName:  "Freeman",
        Age:       100,
        email:     "abraham.freeman@hey.com",
    }

    json, err := json.Marshal(p)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(json))
}

上記プログラムには4つのフィールドを持つPerson構造体があります。main関数では全フィールドを初期化したPersonのインスタンスを生成し、json.Marshalメソッドでp構造体をJSONへ変換しています。このメソッドはバイトスライスかエラーを返すため、JSONデータへアクセスする前に必ずエラー処理が必要です。

Goでバイトスライスを文字列へ変換するには、上記のように型変換を行う必要があります。このプログラムを実行すると、次の出力が得られます。

{"FirstName":"Abraham","LastName":"Freeman","Age":100}

ご覧の通り、任意の用途に使える有効なJSONオブジェクトが取得できています。emailフィールドが結果に含まれていない点に注目してください。これは先頭が小文字のため、Personオブジェクトから外部にエクスポートされないフィールドだからです。

デフォルトでは、Goは構造体のプロパティ名をそのままJSONオブジェクトのフィールド名として使用します。ただし、構造体タグを使えばこれを変更できます。

type Person struct {
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
    Age       int    `json:"age"`
    email     string `json:"email"`
}

上記の構造体タグは、JSONエンコーダに対して構造体のFirstNameプロパティをJSONオブジェクトのfirst_nameフィールドへ対応付けるよう指示しています。前の例にこの変更を加えると、出力は次のようになります。

{"first_name":"Abraham","last_name":"Freeman","age":100}

デコード

JSONオブジェクトをGoの構造体へデコードするには、json.Unmarshal関数を使用します。シグネチャは以下の通りです。

func Unmarshal(data []byte, v interface{}) error

JSONデータのバイトスライスと、デコード結果を格納する場所を受け取ります。デコードが成功すれば、返されるエラーはnilになります。

たとえば次のようなJSONオブジェクトがあるとしましょう。

json := "{"first_name":"John","last_name":"Smith","age":35, "place_of_birth": "London", gender:"male"}"

これを以下のようにPerson構造体のインスタンスへデコードできます。

func main() {
    b := `{"first_name":"John","last_name":"Smith","age":35, "place_of_birth": "London", "gender":"male", "email": "john.smith@hmail.com"}`
    var p Person
    err := json.Unmarshal([]byte(b), &p)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("%+v\n", p)
}

出力は以下のようになります。

{FirstName:John LastName:Smith Age:35 email:}

Unmarshalは、デコード先の型に存在するフィールドのみをデコードします。この例では、place_of_birthgenderPersonのどの構造体フィールドにも対応しないため無視されます。この挙動を利用すれば、大きなJSONオブジェクトから特定のフィールドだけを取り出せます。同様に、デコード先の構造体の非エクスポートフィールドは、JSONオブジェクトに対応するフィールドが存在しても影響を受けません。だからこそ、JSONオブジェクトにemailが存在するにもかかわらず、出力では空文字列のままになっているのです。

データベース

database/sqlパッケージは、SQL(またはSQL風)データベースを扱うための汎用インターフェースを提供します。このパッケージは、ここに挙げられているようなデータベースドライバと組み合わせて使う必要があります。ドライバをインポートする際は、初期化のためにアンダースコア_をプレフィックスとして付ける必要があります。

たとえばMySQLドライバパッケージをdatabase/sqlと併用する場合は次のように書きます。

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

内部的には、ドライバ自身がdatabase/sqlパッケージから利用可能であることを登録しますが、コードから直接使用されることはありません。これにより特定ドライバへの依存が抑えられ、最小限の手間で別のドライバへ差し替えられます。

データベース接続を開く

データベースへアクセスするには、以下のようにsql.DBオブジェクトを作成します。

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
    if err != nil {
        log.Fatal(err)
    }
}

sql.Openメソッドは、後で使えるようデータベース抽象を準備するだけです。実際にデータベースへの接続を確立したり、接続パラメータを検証したりはしません。データベースがすぐに利用可能でアクセスできることを保証したい場合は、db.Ping()メソッドを使用します。

err = db.Ping()
if err != nil {
  log.Fatal(err)
}

データベース接続を閉じる

データベース接続を閉じるにはdb.Close()を使います。通常は、接続を開いた関数(大抵はmain関数)が終了するまで接続を開いたままにするため、deferでクローズを遅延させるのが定石です。

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
    if err != nil {
        log.Fatal(err)
    }
  defer db.Close()
}

sql.DBオブジェクトは長期間使い回せるよう設計されているため、頻繁に開閉すべきではありません。頻繁に開閉すると、接続の再利用・共有の低下、利用可能なネットワークリソースの枯渇、散発的な障害といった問題が起こり得ます。sql.DBはアプリ内で受け渡したりグローバルに公開したりし、データストアへのアクセスが完了した時点で初めて閉じるのがベストプラクティスです。

データベースからのデータ取得

テーブルへのクエリは3ステップで行えます。まずdb.Query()を呼び出し、次に行を反復処理し、最後にrows.Scan()で各行を変数へ展開します。例を見てみましょう。

var (
    id int
    name string
)

rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
    log.Fatal(err)
}

defer rows.Close()

for rows.Next() {
    err := rows.Scan(&id, &name)
    if err != nil {
        log.Fatal(err)
    }

    log.Println(id, name)
}

err = rows.Err()
if err != nil {
    log.Fatal(err)
}

クエリが1行だけ返す場合は、db.Queryの代わりにdb.QueryRowメソッドを使うことで、前述のコードの長ったらしい定型処理を回避できます。

var (
    id int
    name string
)

err = db.QueryRow("select id, name from users where id = ?", 1).Scan(&id, &name)
if err != nil {
    log.Fatal(err)
}

fmt.Println(id, name)

NoSQLデータベース

GoはRedis、MongoDB、CassandraなどのNoSQLデータベースも十分サポートしていますが、それらを扱うための標準インターフェースは提供していません。個々のデータベース専用のドライバパッケージに全面的に頼ることになります。代表的な例をいくつか挙げます。

  • https://github.com/go-redis/redis(Redisドライバ)
  • https://github.com/mongodb/mongo-go-driver(MongoDBドライバ)
  • https://github.com/gocql/gocql(Cassandraドライバ)
  • https://github.com/Shopify/sarama(Apache Kafkaドライバ)

まとめ

本記事では、GoでWebアプリケーションを構築する上で欠かせない重要なポイントをいくつか解説しました。多くのGoプログラマが標準ライブラリを高く評価する理由が、きっと分かったはずです。標準ライブラリは非常に包括的で、本番環境向けサービスに必要なツールの大部分を備えています。

今回扱った内容について不明な点があれば、Twitterで筆者までメッセージを送ってください。シリーズの次回(最終回)の記事では、goツールと、Goでの開発中によくあるタスクへの活用方法について解説します。

ご覧いただきありがとうございました。それでは、Happy Coding!

  1. チャットボットが切り拓くウェブ・アプリ開発の未来

    チャットボット(Chatbot)は、「チャット(会話)ツール」と「ロボット」を掛け合わせた言葉から生まれた概念です。登場から数年が経ちますが、近年になって急速に注目を集め、数え切れないほど多くの企業が導入を進めています。では、チャットボットには一体どのような魅力があるのでしょうか? チャットボットとは、まるで本物の人間のように会話を行うバーチャルロボットサービスです。疲れることなく、常にユーザーの指示に忠実に従い続けます。AI(人工知能)とML(機械学習)を活用して会話のリズムやパターンを学習するため、人間の会話を模倣し、相手の意図や思考を理解した上で適切に反応することができます。つまり、会話

  2. 初心者でも安心!BluefishとKompoZerで始める簡単ウェブ開発

    ホームページを作りたいけれど、何から手をつければいいのか分からない——そんな悩みを抱えていませんか?ウェブサイトをゼロから構築するのは決して簡単な作業ではありません。アイデアが必要で、デザインが必要で、サイトを組み立てるためのツールも必要です。さらに、実際に制作を始める前には、ウェブの言語であるHTMLにある程度慣れておく必要があると思われがちです。しかし、必ずしもそうとは限りません。HTMLやXHTML、CSSなどの関連言語のエキスパートでなくても、十分に見栄えのするウェブサイトは作れます。少なくとも一つの言語に精通していれば確かに有利ですが、意欲さえあれば誰でも今日からスタートできるのです