1章の表1で紹介したように、Goは標準パッケージが充実しています。本章では代表的な標準パッケージをとりあげて、JSON、ファイル、HTTP、テンプレートの扱いを解説します。最後はそれらを組み合わせて簡単なHTTPサーバを作成します。
encoding/jsonパッケージ
JSONを扱うためにはencoding/jsonパッケージを用います。主なAPIとして、構造体をJSONに変換するMarshal()
と、その逆を行うUnmarshal()
が提供されています。
構造体からJSONへの変換
まずは次のような構造体を用意します。
この構造体にデータを代入し、ポインタをjson.Marshal()
に渡すだけで、デフォルトのフォーマットでJSON文字列の[]byte
を生成できます。
出力されたJSONは、整形すると次のようになります。
タグ付け
変換されたJSONを見てみると、キーの名前は構造体のフィールド名と同じになっており、小文字で始まるプライベートなフィールドはJSONには含まれていないことがわかります。もし、プライベートではない構造体のフィールドもJSONに出力しないようにしたり、出力されるキーの名前を変えたい場合などは、構造体にタグを記述することで出力をコントロールできます。
encoding/jsonパッケージで使用できるタグは、次のようなものがあります。
タグは次のように、フィールドの型定義の後ろに記述します。
出力結果は次のように変わります。
JSONから構造体への変換
逆にJSONの文字列からデータをマップした構造体を生成するには、json.Unmarshal()
を使用します。格納するJSONと、格納先の構造体のポインタを引数として渡すと、タグの定義に従って構造体のフィールドに値が格納されます。
os、ioパッケージ
次に、ファイルへの書き込みと読み出しを行い、先ほどのJSONをファイルに保存するようにしましょう。ファイルの作成や取得はosパッケージ、書き込みや読み出しはioパッケージを用いて行います。
ファイルの生成
まずosパッケージを用いてファイルを作成してみましょう。os.Create()
関数にファイル名を渡すと、*os.File
構造体へのポインタが取得できます。このとき第二戻り値にエラーが返るため最初にエラー処理をします。
*os.File
は、io.ReadWriteCloser
というインタフェース型であり、これは、Read()
、Write()
、Close()
の3つのメソッドを実装していることを意味します。開いたファイルは使い終わったら閉じる必要があるので、defer
を用いてClose()
をmain()
の終わりで実行するようにします。
ファイルへの書き込み
続いてファイルにデータを書き込んでみましょう。
先ほど取得した*os.File
は、io.Writer
インタフェースを実装していました。これは次のように定義されています。
[]byte
を引数として渡すと、その中身を対象に書き込み、戻り値として書き込んだバイト数とエラーを返します。
これを利用して、hello world
を書き込むには次のようにします。
ここでは、Write()
の第一戻り値である書き込まれたバイト数は無視していますが、第二戻り値であるエラーは受け取ってエラー処理をしています。
実行し、生成されたファイルに文字列が書き込まれていれば成功です。
また、WriteString()
を用いると、毎回[]byte
に変換する必要がなくなります。
書き込む対象のio.Writer
がWriteString()
のようなメソッドを実装していない場合は、fmt.Fprint()
を用いると、[]byte
を経由せずio.Writer
に対して文字列を直接書き込むことができます。
このようにデータを書き込む方法はいくつかありますが、いずれも対象がio.Writer
インタフェース型であることを利用している点を意識するとよいでしょう。
ファイルからの読み出し
次に書き込んだデータを読み出してみます。すでにあるファイルを開く場合はos.Open()
を用います。
ファイルの読み出しにはio.Reader
インタフェースを用います。
Read()
は、読み出したデータを格納するのに十分な長さを持ったスライスを渡すと、そこにデータが格納されます。ここではhello world\n
という12byteのデータを読み出すため、長さが12のbyteのスライスを用意し、そこにデータを読み出します。戻り値として読み出したバイト数とエラーを返します(ここでは、戻り値のバイト数のほうは無視します)。
以上が、*os.File
が実装しているio.ReadWrite Closer
インタフェースを用いた最も基本的なファイル操作です。これはファイル操作以外に、ネットワークへのI/O処理でも同様に役に立つ知識です。
JSONのEncoder/Decoder経由の保存
ここまでの2つを組み合わせると、JSONをファイルに保存するには、JSONとファイルの間でデータを[]byte
でやりとりすればよいことは容易に想像できるでしょう。しかし、JSONにはEncoder/Decoderというio.ReadeWriter
を扱うAPIも用意されているため、同じくio.ReadWriter
であるファイルを扱うにはこちらを用いることができます。
まず、json.Encoder
を用いてJSONにデータを変換しつつ、io.Write()
経由でファイルに書き込んでみましょう。
JSONへの変換結果を[]byte
として受け取ることなく、そのまま*os.File
に書き込んでいることがわかると思います。
同様に、json.Decoder
を用いてファイル内のJSONデータを読み出し、Person
にデコードしてみましょう。
こちらも、ファイルから読み出した結果を[]byte
として受け取ることなく、そのままデコードしてPerson
型の変数に格納しています。
このように、標準ライブラリにはio.Reader
、io.Writer
を中心として設計されたAPIが多くあります。この点を意識してドキュメントを見ると、より適したAPIを選ぶことができます。
io/ioutilパッケージ
ファイルの操作は、io/ioutilパッケージを用いるとより簡単に行うことができます。
全体の読み出し
ioutil.ReadAll()
は、引数にio.Reader
を渡すと、その中身をすべて読み出し、[]byte
型で返します。先ほどの*os.File
の読み出しでは直接Read()
を呼んでいたため十分な長さの[]byte
を用意する必要がありましたが、これを用いると不要になります。
これはio.Reader
を実装したすべての型で使用できるため、ファイルI/O以外のネットワークプログラミングなどでも重宝します。
ファイルの読み書き
ファイル操作に特化したメソッドも用意されています。
ioutil.ReadFile()
は、ファイル名を指定するとその中身をすべて[]byte
として読み出します。
iotuil.WriteFile()
は、ファイル名を指定するとそこに[]byte
型のデータを書き込みます。第三引数にはファイルのパーミッションを8進数で指定します。
これらを用いて先ほどの操作を書き直すと、次のようになります。
特にファイルI/Oを扱う場合は、io/ioutilパッケージは非常に便利なので覚えておくとよいでしょう。
net/httpパッケージ
net/httpパッケージには、HTTPサーバやクライアントを作成するためのAPIがそろっています。ここでは、このパッケージを用いて簡単なHTTPサーバを実装してみたいと思います[1]。
hello world サーバ
まず、net/httpパッケージを用いてhello worldを返す簡単なサーバを実装すると、次のようになります。
ここでは、最初にhttp.HandleFunc()
でルーティングの設定をします。http.HandleFunc()
は次のような定義になっています。
第一引数はパスのパターンで、リクエストを受けたときはこのパターンに一致したハンドラが実行されます。今回はルートパス(/
)に対するリクエストに処理を登録します。
第二引数は2つの引数を受け取る関数になっており、ここではIndexHandler
で実装しています。Request
にはリクエストの情報が入っており、それをもとに組み立てた結果をResponseWriter
に書き込むことでレスポンスを返せます。ResponseWriter
は名前のとおりio.Writer
なので、ここではfmt.Fprint()
を用いて文字列を書き込んでいます。
最後にmain()
では、http.ListenAndServe()
にポートを指定してサーバを起動しています。第二引数は今回は使わないためnil
を指定します。
このプログラムを実行し、ブラウザからhttp://localhost:3000/
にアクセスして、hello world
が表示されれば成功です(図1)。
JSON/HTMLサーバ
ここではPOSTで送信されたJSONデータをファイルに保存し、リクエストに応じてファイルから読み出したデータをHTMLに格納して返す、ごく簡単なHTTPサーバを実装してみましょう。
このサーバは、次のようなPersonデータを扱うことにします。
サーバは、クライアントからPerson型のJSONをPOSTで受け取ると、id値に対応したファイルを作成し、そこにNameの値を格納します。またGETでは、クエリパラメータとしてidで指定された値のファイルを読み出し、HTMLに整形して返します。
POST
処理はPersonHandler
に実装し、それを/persons
のパスに対して登録します。ここではPOSTリクエストを処理するため、http.Request.Method
の値で分岐し、JSON内のIDの値からファイルを作り、その中にNameの値を書き込んでいます。
処理が成功した場合はレスポンスとして201 CREATED
を返すため、ResponseWriter.WriteHeader()
にnet/httpパッケージに定義されたステータスコードを指定してレスポンスを返します。
サーバを起動したら、JSONをPOSTで送ってみましょう。curlコマンドが使える環境では、次の方法でリクエストできます。
idを1としたため、成功していれば1.txtが作られ、中にgopher
が格納されているはずです。
GET
GETが来た場合は、クエリパラメータのid値を用いて該当するファイルに格納された名前を読み出し、その情報をHTMLに埋め込んでレスポンスします。
クエリパラメータはResponseWriter.URL.Query().Get()
から取得できます。この値は文字列であるため、数値に変換するにはstrconvパッケージのAtoi()
を用います。
ここでは、指定されたid
に対するファイルを開き、その中に格納された値を埋め込んだHTMLを返すようにしましょう。HTMLの生成には標準のテンプレートエンジンを使用します。テンプレートエンジンの使い方は次節で解説します。
html/templateパッケージ
Goは、text/templateパッケージとhtml/templateパッケージの2つのテンプレートエンジンが付属しており、どちらも同じインタフェースで使用できます。
2つの違いは、html/templateパッケージの場合はHTMLのエスケープ処理が自動で行われる点です。XSS(Cross-Site Scripting、クロスサイトスクリプティング)などの脆弱性を埋め込みにくくすることができます。したがって、Webの用途ではhtml/templateパッケージを用います。
テンプレートの作成
テンプレートはJinja2というテンプレートエンジンと似た記法で記述します。データを埋め込むには{{ }}
で値をくくり、その中に埋め込みたいプロパティを指定します。ここでは、Personの構造体の値を埋め込むテンプレートを、index.html
に作成します。
テンプレートのコンパイルはParseFiles()
という関数を使います。
ParseFiles()
は戻り値としてエラーを一緒に返しますが、Must()
を一緒に用いるとエラー時に戻り値ではなくパニックを発生します。一度コンパイルが通ることを確認したテンプレートであれば、毎回エラー処理をする必要性は低いため、Must()
を合わせて利用することがよくあります。
テンプレートへの値の埋め込み
コンパイルしたテンプレートに実際に値を埋め込むには、Execute()
を用います。ここでは第二引数に渡したperson
がテンプレートに適用され、たとえば{{ .ID }}
の部分にはperson.ID
の値が適用されます。第一引数はio.Writer
を渡すと、値を埋め込んだ結果をそこに書き込みます。ここでは結果をレスポンスとして送信するためResponseWriter
を直接指定します。
サーバを起動したら、ブラウザからhttp://localhost:3000/persons?id=1
にアクセスすると、結果が表示されます(図2・注2)。
まとめ
本章では、よく使う標準パッケージを用いてHTTPサーバを作成する方法を解説しました。特にioパッケージのインタフェースの扱いは、ほかのパッケージを用いる際にも必要となる場合があるため、覚えておくとよいでしょう。