Struct タグについて

たとえば struct で構造化されている情報を特定のファイルやデータベースに出力したり,逆にファイルやデータベースの情報を struct に流し込みたい場合に struct の各フィールドに目印になる情報があると便利である。 この目印として機能するのが struct タグである1

struct タグは以下のように記述する。

By convention, tag strings are a concatenation of optionally space-separated key:"value" pairs. Each key is a non-empty string consisting of non-control characters other than space (U+0020 ' '), quote (U+0022 '"'), and colon (U+003A ':'). Each value is quoted using U+0022 '"' characters and Go string literal syntax.
type Server struct {
    Host      string `elem:"host"`
    IPAddress string `elem:"ip_address"`
    Port      int    `elem:"port"`
    Note      string `elem:"note"`
}

このタグ情報を取得するには reflect パッケージを使う。 たとえばこんな感じ。

package main

import (
    "fmt"
    "reflect"
)

type Server struct {
    Host      string `elem:"host"`
    IPAddress string `elem:"ip_address"`
    Port      int    `elem:"port"`
    Note      string `elem:"note"`
}

func main() {
    s := Server{}
    t := reflect.TypeOf(s)
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Name=%s , tag(elem)=%s\n", field.Name, field.Tag.Get("elem"))
    }
}

これを実行するとこうなる。

Name=Host , tag(elem)=host
Name=IPAddress , tag(elem)=ip_address
Name=Port , tag(elem)=port
Name=Note , tag(elem)=note

実際には reflect を直接使う局面は少なく,既にあるパッケージを利用することが多い。 たとえば struct による構造化データを JSON 形式に出力する encoding/json パッケージがある。

package main

import (
    "encoding/json"
    "fmt"
)

type Server struct {
    Host      string `json:"host"`
    IPAddress string `json:"ip_address"`
    Port      int    `json:"port"`
    Note      string `json:"note"`
}

func main() {
    s := Server{Host: "localhost", IPAddress: "127.0.0.1", Port: 8080, Note: "Web Application"}
    j, err := json.MarshalIndent(s, "", "  ")
    if err != nil {
        return
    }
    fmt.Println(string(j))
}

これを実行するとこうなる。

{
  "host": "localhost",
  "ip_address": "127.0.0.1",
  "port": 8080,
  "note": "Web Application"
}

Server の内容が JSON 形式で出力されているのが分かるだろう。 JSON の要素名がタグで指定した名前になっていることを確認してほしい。

反対もやってみよう。

package main

import (
    "encoding/json"
    "fmt"
)

type Server struct {
    Host      string `json:"host"`
    IPAddress string `json:"ip_address"`
    Port      int    `json:"port"`
    Note      string `json:"note"`
}

func main() {
    svr := []byte(`{
  "host": "localhost",
  "ip_address": "127.0.0.1",
  "port": 8080,
  "note": "Web Application"
}`)
    var s Server
    if err := json.Unmarshal(svr, &s); err != nil {
        return
    }
    fmt.Println(s)
}

実行結果はこうなる。

{localhost 127.0.0.1 8080 Web Application}

きれいに struct に値が入っているのが分かると思う。

ちなみにタグの書式は key:"value" だが,間違って記述しても単に無視されるだけでコンパイル時も実行時もエラーにならないので注意が必要である。 なおタグ書式の文法ミスについては,静的検査ツールの vet でチェックできる。

タグは複数列挙することができる。 たとえばサンプルの構造体を TOML にも対応させたいなら

type Server struct {
    Host      string `json:"host" toml:"host"`
    IPAddress string `json:"ip_address" toml:"ip_address"`
    Port      int    `json:"port" toml:"port"`
    Note      string `json:"note" toml:"note"`
}

などとする(デリミタは空白文字)。 じゃあ,先ほどと同じようにして TOML で出力してみる。 TOML を扱うには github.com/BurntSushi/toml パッケージを使うとよい。

package main

import (
    "bytes"
    "fmt"

    "github.com/BurntSushi/toml"
)

type Server struct {
    Host      string `json:"host" toml:"host"`
    IPAddress string `json:"ip_address" toml:"ip_address"`
    Port      int    `json:"port" toml:"port"`
    Note      string `json:"note" toml:"note,omitempty"`
}

func main() {
    s := Server{Host: "localhost", IPAddress: "127.0.0.1", Port: 8080, Note: ""}
    t := new(bytes.Buffer)
    if err := toml.NewEncoder(t).Encode(s); err != nil {
        return
    }
    fmt.Println(t.String())
}

実行結果は以下の通り。

host = "localhost"
ip_address = "127.0.0.1"
port = 8080

omitempty オプションはフィールドが空(nil または空文字列)の場合に出力を省略できる2。 このオプションは encoding/json パッケージでも使える。

ついでに反対もやってみよう。

package main

import (
    "fmt"

    "github.com/BurntSushi/toml"
)

type Server struct {
    Host      string `json:"host" toml:"host"`
    IPAddress string `json:"ip_address" toml:"ip_address"`
    Port      int    `json:"port" toml:"port"`
    Note      string `json:"note" toml:"note,omitempty"`
}

func main() {
    svr := `
host = "localhost"
ip_address = "127.0.0.1"
port = 8080
note = "Web Application"
`
    var s Server
    if _, err := toml.Decode(svr, &s); err != nil {
        return
    }
    fmt.Println(s)
}

結果は以下の通り。

{localhost 127.0.0.1 8080 Web Application}

このように struct で正規化できる情報であれば,タグ機能を使うことでアプリケーション外部とのやり取りがだいぶ楽になる。

ブックマーク

Go 言語に関するブックマーク集はこちら


  1. 「アノテーション(annotation)」と呼ぶ人もいる。たぶん Java の annotation 機能を意識しているんだろう。 ↩︎

  2. 数値の場合は omitzero オプションを付けると 0 のときに出力を省略できる。ただし BurntSushi/toml パッケージでは Decode() がうまく動かないらしい。実は omitempty オプションも Decode() 時の挙動が怪しいんだよなぁ。 TOML パーサの別実装としては naoina/toml というのもある。これは最新の TOML 仕様に追随しているようだが omitzero オプションには対応していない。 ↩︎