はじめてのGo―シンプルな言語仕様、型システム、並行処理

第4章標準パッケージ―JSON、ファイル、HTTP、HTMLを扱う

1章表1で紹介したように、Goは標準パッケージが充実しています。本章では代表的な標準パッケージをとりあげて、JSON、ファイル、HTTP、テンプレートの扱いを解説します。最後はそれらを組み合わせて簡単なHTTPサーバを作成します。

encoding/jsonパッケージ

JSONを扱うためにはencoding/jsonパッケージを用います。主なAPIとして、構造体をJSONに変換するMarshal()と、その逆を行うUnmarshal()が提供されています。

構造体からJSONへの変換

まずは次のような構造体を用意します。

type Person struct {
    ID int
    Name string
    Email string
    Age int
    Address string
    memo string
}

この構造体にデータを代入し、ポインタをjson.Marshal()に渡すだけで、デフォルトのフォーマットでJSON文字列の[]byteを生成できます。

func main() {
    person := &Person{
        ID: 1,
        Name: "Gopher",
        Email: "[email protected]",
        Age: 5,
        Address: "",
        memo: "golang lover",
    }
    b, err := json.Marshal(person)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b)) // 文字列に変換
}

出力されたJSONは、整形すると次のようになります。

{
  "ID": 1,
  "Name": "Gopher",
  "Email": "[email protected]",
  "Age": 5,
  "Address": ""
}

タグ付け

変換されたJSONを見てみると、キーの名前は構造体のフィールド名と同じになっており、小文字で始まるプライベートなフィールドはJSONには含まれていないことがわかります。もし、プライベートではない構造体のフィールドもJSONに出力しないようにしたり、出力されるキーの名前を変えたい場合などは、構造体にタグを記述することで出力をコントロールできます。

encoding/jsonパッケージで使用できるタグは、次のようなものがあります。

`json:"name"` // nameというキーで格納する
`json:"-"` // JSONに格納しない
`json:",omitempty"` // 値が空なら無視
`json:",string"` // 値をJSONとして格納

タグは次のように、フィールドの型定義の後ろに記述します。

type Person struct {
    ID int `json:"id"`
    Name string `json:"name"`
    Email string `json:"-"`
    Age int `json:"age"`
    Address string `json:"address,omitempty"`
    memo string
}

出力結果は次のように変わります。

fmt.Println(string(b))
// {"id":1,"name":"Gopher","age":5}

JSONから構造体への変換

逆にJSONの文字列からデータをマップした構造体を生成するには、json.Unmarshal()を使用します。格納するJSONと、格納先の構造体のポインタを引数として渡すと、タグの定義に従って構造体のフィールドに値が格納されます。

func main() {
    var person Person
    b := []byte(`{"id":1,"name":"Gopher","age":5}`)
    err := json.Unmarshal(b, &person)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(person) // {1 Gopher 5 }
}

os、ioパッケージ

次に、ファイルへの書き込みと読み出しを行い、先ほどのJSONをファイルに保存するようにしましょう。ファイルの作成や取得はosパッケージ、書き込みや読み出しはioパッケージを用いて行います。

ファイルの生成

まずosパッケージを用いてファイルを作成してみましょう。os.Create()関数にファイル名を渡すと、*os.File構造体へのポインタが取得できます。このとき第二戻り値にエラーが返るため最初にエラー処理をします。

package main

import (
    "log"
    "os"
)

func main() {
    // ファイルを生成
    file, err := os.Create("./file.txt")
    if err != nil { // エラー処理
        log.Fatal(err)
    }
    // プログラムが終わったらファイルを閉じる
    defer file.Close()
}

*os.Fileは、io.ReadWriteCloserというインタフェース型であり、これは、Read()Write()Close()の3つのメソッドを実装していることを意味します。開いたファイルは使い終わったら閉じる必要があるので、deferを用いてClose()main()の終わりで実行するようにします。

ファイルへの書き込み

続いてファイルにデータを書き込んでみましょう。

先ほど取得した*os.Fileは、io.Writerインタフェースを実装していました。これは次のように定義されています。

type Writer interface {
    Write(p []byte) (n int, err error)
}

[]byteを引数として渡すと、その中身を対象に書き込み、戻り値として書き込んだバイト数とエラーを返します。

これを利用して、hello worldを書き込むには次のようにします。

func main() {
    // ファイルを生成
    file, err := os.Create("./file.txt")
    if err != nil { // エラー処理
        log.Fatal(err)
    }

    // プログラムが終わったらファイルを閉じる
    defer file.Close()

    // 書き込むデータを[]byteで用意する
    message := []byte("hello world\n")

    // Write()を用いて書き込む
    _, err = file.Write(message)
    if err != nil { // エラー処理
        log.Fatal(err)
    }
}

ここでは、Write()の第一戻り値である書き込まれたバイト数は無視していますが、第二戻り値であるエラーは受け取ってエラー処理をしています。

実行し、生成されたファイルに文字列が書き込まれていれば成功です。

$ go run write.go
$ cat file.txt
hello world

また、WriteString()を用いると、毎回[]byteに変換する必要がなくなります。

_, err = file.WriteString("hello world\n")

書き込む対象のio.WriterWriteString()のようなメソッドを実装していない場合は、fmt.Fprint()を用いると、[]byteを経由せずio.Writerに対して文字列を直接書き込むことができます。

_, err = fmt.Fprint(file, "hello world\n")

このようにデータを書き込む方法はいくつかありますが、いずれも対象がio.Writerインタフェース型であることを利用している点を意識するとよいでしょう。

ファイルからの読み出し

次に書き込んだデータを読み出してみます。すでにあるファイルを開く場合はos.Open()を用います。

func main() {
    // ファイルを開く
    file, err := os.Open("./file.txt")
    if err != nil { // エラー処理
        log.Fatal(err)
    }

    // プログラムが終わったらファイルを閉じる
    defer file.Close()
}

ファイルの読み出しにはio.Readerインタフェースを用います。

type Reader interface {
    Read(p []byte) (n int, err error)
}

Read()は、読み出したデータを格納するのに十分な長さを持ったスライスを渡すと、そこにデータが格納されます。ここではhello world\nという12byteのデータを読み出すため、長さが12のbyteのスライスを用意し、そこにデータを読み出します。戻り値として読み出したバイト数とエラーを返します(ここでは、戻り値のバイト数のほうは無視します⁠⁠。

func main() {
    // ファイルを開く
    file, err := os.Open("./file.txt")
    if err != nil { // エラー処理
        log.Fatal(err)
    }

    // プログラムが終わったらファイルを閉じる
    defer file.Close()

    // 12byte格納可能なスライスを用意する
    message := make([]byte, 12)

    // ファイル内のデータをスライスに読み出す
    _, err = file.Read(message)
    if err != nil { // エラー処理
        log.Fatal(err)
    }

    // 文字列にして表示
    fmt.Print(string(message))
}

以上が、*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()経由でファイルに書き込んでみましょう。

func main() {
    person := &Person{
        ID: 1,
        Name: "Gopher",
        Email: "[email protected]",
        Age: 5,
        Address: "",
        memo: "golang lover",
    }

    // ファイルを開く
    file, err := os.Create("./person.json")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // エンコーダの取得
    encoder := json.NewEncoder(file)

    // JSONエンコードしたデータの書き込み
    err = encoder.Encode(person)
    if err != nil {
        log.Fatal(err)
    }
}

JSONへの変換結果を[]byteとして受け取ることなく、そのまま*os.Fileに書き込んでいることがわかると思います。

同様に、json.Decoderを用いてファイル内のJSONデータを読み出し、Personにデコードしてみましょう。

func main() {
    // ファイルを開く
    file, err := os.Open("./person.json")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // データを書き込む変数
    var person Person

    // デコーダの取得
    decoder := json.NewDecoder(file)

    // JSONデコードしたデータの書き込み
    err = decoder.Decode(&person)
    if err != nil {
        log.Fatal(err)
    }

    // 読み出した結果の表示
    fmt.Println(person)
}

こちらも、ファイルから読み出した結果を[]byteとして受け取ることなく、そのままデコードしてPerson型の変数に格納しています。

このように、標準ライブラリにはio.Readerio.Writerを中心として設計されたAPIが多くあります。この点を意識してドキュメントを見ると、より適したAPIを選ぶことができます。

io/ioutilパッケージ

ファイルの操作は、io/ioutilパッケージを用いるとより簡単に行うことができます。

全体の読み出し

ioutil.ReadAll()は、引数にio.Readerを渡すと、その中身をすべて読み出し、[]byte型で返します。先ほどの*os.Fileの読み出しでは直接Read()を呼んでいたため十分な長さの[]byteを用意する必要がありましたが、これを用いると不要になります。

// ファイルの中身をすべて読み出す
file, _ := os.Open("./file.txt")
message, err := ioutil.ReadAll(file)

これはio.Readerを実装したすべての型で使用できるため、ファイルI/O以外のネットワークプログラミングなどでも重宝します。

ファイルの読み書き

ファイル操作に特化したメソッドも用意されています。

ioutil.ReadFile()は、ファイル名を指定するとその中身をすべて[]byteとして読み出します。

// ファイルの中身をすべて読み出す
message, err := ioutil.ReadFile("./file.txt")

iotuil.WriteFile()は、ファイル名を指定するとそこに[]byte型のデータを書き込みます。第三引数にはファイルのパーミッションを8進数で指定します。

message := []byte("hello world\n")
err := ioutil.WriteFile("./file.txt", message, 0666)

これらを用いて先ほどの操作を書き直すと、次のようになります。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
)

func main() {
    // ファイルにメッセージを書き込む
    hello := []byte("hello world\n")
    err := ioutil.WriteFile("./file.txt", hello, 0666)
    if err != nil { // エラー処理
        log.Fatal(err)
    }

// ファイルの中身を全て読み出す
    message, err := ioutil.ReadFile("./file.txt")
    if err != nil { // エラー処理
        log.Fatal(err)
    }
    fmt.Print(string(message)) // 文字列にして表示
}

特にファイルI/Oを扱う場合は、io/ioutilパッケージは非常に便利なので覚えておくとよいでしょう。

net/httpパッケージ

net/httpパッケージには、HTTPサーバやクライアントを作成するためのAPIがそろっています。ここでは、このパッケージを用いて簡単なHTTPサーバを実装してみたいと思います[1]⁠。

hello world サーバ

まず、net/httpパッケージを用いてhello worldを返す簡単なサーバを実装すると、次のようになります。

package main

import (
    "fmt"
    "net/http"

)

func IndexHandler(w http.ResponseWriter,
    r *http.Request) {

    fmt.Fprint(w, "hello world")
}

func main() {
    http.HandleFunc("/", IndexHandler)
    http.ListenAndServe(":3000", nil)
}

ここでは、最初にhttp.HandleFunc()でルーティングの設定をします。http.HandleFunc()は次のような定義になっています。

func HandleFunc(pattern string,
    handler func(ResponseWriter, *Request))

第一引数はパスのパターンで、リクエストを受けたときはこのパターンに一致したハンドラが実行されます。今回はルートパス/に対するリクエストに処理を登録します。

第二引数は2つの引数を受け取る関数になっており、ここではIndexHandlerで実装しています。Requestにはリクエストの情報が入っており、それをもとに組み立てた結果をResponseWriterに書き込むことでレスポンスを返せます。ResponseWriterは名前のとおりio.Writerなので、ここではfmt.Fprint()を用いて文字列を書き込んでいます。

最後にmain()では、http.ListenAndServe()にポートを指定してサーバを起動しています。第二引数は今回は使わないためnilを指定します。

このプログラムを実行し、ブラウザからhttp://localhost:3000/にアクセスして、hello worldが表示されれば成功です図1⁠。

$ go run server.go
図1 hello-server
図1 hello-server

JSON/HTMLサーバ

ここではPOSTで送信されたJSONデータをファイルに保存し、リクエストに応じてファイルから読み出したデータをHTMLに格納して返す、ごく簡単なHTTPサーバを実装してみましょう。

このサーバは、次のようなPersonデータを扱うことにします。

type Person struct {
    ID int `json:"id"`
    Name string `json:"name"`
}

サーバは、クライアントからPerson型のJSONをPOSTで受け取ると、id値に対応したファイルを作成し、そこにNameの値を格納します。またGETでは、クエリパラメータとしてidで指定された値のファイルを読み出し、HTMLに整形して返します。

POST

処理はPersonHandlerに実装し、それを/personsのパスに対して登録します。ここではPOSTリクエストを処理するため、http.Request.Methodの値で分岐し、JSON内のIDの値からファイルを作り、その中にNameの値を書き込んでいます。

処理が成功した場合はレスポンスとして201 CREATEDを返すため、ResponseWriter.WriteHeader()にnet/httpパッケージに定義されたステータスコードを指定してレスポンスを返します。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
)

type Person struct {
    ID int `json:"id"`
    Name string `json:"name"`
}

func IndexHandler(w http.ResponseWriter,
    r *http.Request) {

    fmt.Fprint(w, "hello world")
}

func PersonHandler(w http.ResponseWriter,
    r *http.Request) {
    defer r.Body.Close() // 処理の最後にBodyを閉じる

    if r.Method == "POST" {
        // リクエストボディをJSONに変換
        var person Person
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&person)
        if err != nil { // エラー処理
            log.Fatal(err)
        }

        // ファイル名を {id}.txtとする
        filename := fmt.Sprintf("%d.txt", person.ID)
        file, err := os.Create(filename) // ファイルを生成
        if err != nil {
            log.Fatal(err)
        }
        defer file.Close()

        // ファイルにNameを書き込む
        _, err = file.WriteString(person.Name)
        if err != nil {
            log.Fatal(err)
        }

        // レスポンスとしてステータスコード201を送信
        w.WriteHeader(http.StatusCreated)
    }
}

func main() {
    http.HandleFunc("/", IndexHandler)
    http.HandleFunc("/persons", PersonHandler)
    http.ListenAndServe(":3000", nil)
}

サーバを起動したら、JSONをPOSTで送ってみましょう。curlコマンドが使える環境では、次の方法でリクエストできます。

$ curl http://localhost:3000/persons -d '{"id":1,"name":"gopher"}' 

idを1としたため、成功していれば1.txtが作られ、中にgopherが格納されているはずです。

$ cat 1.txt
gopher

GET

GETが来た場合は、クエリパラメータのid値を用いて該当するファイルに格納された名前を読み出し、その情報をHTMLに埋め込んでレスポンスします。

クエリパラメータはResponseWriter.URL.Query().Get()から取得できます。この値は文字列であるため、数値に変換するにはstrconvパッケージのAtoi()を用います。

// パラメータを取得
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
    log.Fatal(err)
}

ここでは、指定されたidに対するファイルを開き、その中に格納された値を埋め込んだHTMLを返すようにしましょう。HTMLの生成には標準のテンプレートエンジンを使用します。テンプレートエンジンの使い方は次節で解説します。

html/templateパッケージ

Goは、text/templateパッケージとhtml/templateパッケージの2つのテンプレートエンジンが付属しており、どちらも同じインタフェースで使用できます。

2つの違いは、html/templateパッケージの場合はHTMLのエスケープ処理が自動で行われる点です。XSSCross-Site Scriptingクロスサイトスクリプティング)などの脆弱性を埋め込みにくくすることができます。したがって、Webの用途ではhtml/templateパッケージを用います。

テンプレートの作成

テンプレートはJinja2というテンプレートエンジンと似た記法で記述します。データを埋め込むには{{ }}で値をくくり、その中に埋め込みたいプロパティを指定します。ここでは、Personの構造体の値を埋め込むテンプレートを、index.htmlに作成します。

<!DOCTYPE html>
<title>person</title>
<h1>{{ .ID }} : {{ .Name }}</h1>

テンプレートのコンパイルはParseFiles()という関数を使います。

t, err := template.ParseFiles("index.html")

ParseFiles()は戻り値としてエラーを一緒に返しますが、Must()を一緒に用いるとエラー時に戻り値ではなくパニックを発生します。一度コンパイルが通ることを確認したテンプレートであれば、毎回エラー処理をする必要性は低いため、Must()を合わせて利用することがよくあります。

var t = template.Must(template.ParseFiles("index.html"))

テンプレートへの値の埋め込み

コンパイルしたテンプレートに実際に値を埋め込むには、Execute()を用います。ここでは第二引数に渡したpersonがテンプレートに適用され、たとえば{{ .ID }}の部分にはperson.IDの値が適用されます。第一引数はio.Writerを渡すと、値を埋め込んだ結果をそこに書き込みます。ここでは結果をレスポンスとして送信するためResponseWriterを直接指定します。

// テンプレートのコンパイル
var t = template.Must(template.ParseFiles("index.html"))

func PersonHandler(w http.ResponseWriter,
    r *http.Request) {
    defer r.Body.Close() // 処理の最後にBodyを閉じる

    if r.Method == "POST" {
        // リクエストボディをJSONに変換
        var person Person
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&person)
        if err != nil { // エラー処理
            log.Fatal(err)
        }

        // ファイル名を{id}.txtとする
        filename := fmt.Sprintf("%d.txt", person.ID)
        file, err := os.Create(filename) // ファイルを生成
        if err != nil {
            log.Fatal(err)
        }
        defer file.Close()

        // ファイルにNameを書き込む
        _, err = file.WriteString(person.Name)
        if err != nil {
            log.Fatal(err)
        }

        // レスポンスとしてステータスコード201を送信
        w.WriteHeader(http.StatusCreated)
} else if r.Method == "GET" {
        // パラメータを取得
        id, err := strconv.Atoi(r.URL.Query().Get("id"))
        if err != nil {
            log.Fatal(err)
        }

        filename := fmt.Sprintf("%d.txt", id)
        b, err := ioutil.ReadFile(filename)
        if err != nil {
            log.Fatal(err)
        }

        // personを生成
        person := Person{
            ID: id,
            Name: string(b),
        }

        // レスポンスにエンコーディングしたHTMLを書き込む
        t.Execute(w, person)
    }
}

サーバを起動したら、ブラウザからhttp://localhost:3000/persons?id=1にアクセスすると、結果が表示されます図2・注2⁠。

図2 html-server
図2 html-server

まとめ

本章では、よく使う標準パッケージを用いてHTTPサーバを作成する方法を解説しました。特にioパッケージのインタフェースの扱いは、ほかのパッケージを用いる際にも必要となる場合があるため、覚えておくとよいでしょう。

おすすめ記事

記事・ニュース一覧