GoでStructタグを使用する方法

前書き

構造体または構造体は、複数の情報を1つのユニットに収集するために使用されます。 これらのcollections of informationは、StreetCityState、およびPostalCodeで構成されるAddressなどの高レベルの概念を説明するために使用されます。 。 データベースやAPIなどのシステムからこの情報を読み取る場合、structタグを使用して、この情報をstructのフィールドに割り当てる方法を制御できます。 構造体タグは、構造体のフィールドに付加されるメタデータの小さな断片であり、構造体で動作する他のGoコードに指示を提供します。

Structタグはどのように見えますか?

Go構造体タグは、Go構造体宣言の型の後に表示される注釈です。 各タグは、対応する値に関連付けられた短い文字列で構成されます。

構造体タグは次のようになり、タグはバッククォート+\ + `文字でオフセットされます。

type User struct {
    Name string `example:"name"`
}

他のGoコードは、これらの構造体を調べて、要求する特定のキーに割り当てられた値を抽出できます。 Structタグは、コードを検査する他のコードがない限り、コードの動作に影響を与えません。

この例を試して、structタグがどのように見えるかを確認してください。また、別のパッケージのコードがなければ、効果はありません。

package main

import "fmt"

type User struct {
    Name string `example:"name"`
}

func (u *User) String() string {
    return fmt.Sprintf("Hi! My name is %s", u.Name)
}

func main() {
    u := &User{
        Name: "Sammy",
    }

    fmt.Println(u)
}

これは出力されます:

OutputHi! My name is Sammy

この例では、Nameフィールドを持つUserタイプを定義します。 Nameフィールドには、example:"name"の構造体タグが与えられています。 会話でこの特定のタグを「example struct tag」と呼びます。これは、「example」という単語をキーとして使用するためです。 example構造体タグのNameフィールドの値は"name"です。 Userタイプでは、fmt.Stringerインターフェイスに必要なString()メソッドも定義します。 これは、型をfmt.Printlnに渡すと自動的に呼び出され、適切にフォーマットされたバージョンの構造体を生成する機会を与えてくれます。

mainの本体内に、Userタイプの新しいインスタンスを作成し、それをfmt.Printlnに渡します。 構造体にはstructタグが存在していましたが、このGoコードの操作には影響がないことがわかります。 structタグが存在しない場合、まったく同じように動作します。

構造体タグを使用して何かを達成するには、実行時に構造体を調べる他のGoコードを記述する必要があります。 標準ライブラリには、操作の一部としてstructタグを使用するパッケージがあります。 これらの中で最も人気のあるものはencoding/jsonパッケージです。

JSONのエンコード

JavaScript Object Notation(JSON)は、さまざまな文字列キーで整理されたデータのコレクションをエンコードするためのテキスト形式です。 さまざまなプログラムでデータを通信するために一般的に使用されます。形式が非常に単純なので、多くの異なる言語でデコードできるライブラリが存在します。 以下はJSONの例です。

{
  "language": "Go",
  "mascot": "Gopher"
}

このJSONオブジェクトには、languagemascotの2つのキーが含まれています。 これらのキーに続いて、関連付けられた値があります。 ここで、languageキーの値はGoであり、mascotには値Gopherが割り当てられています。

標準ライブラリのJSONエンコーダーは、JSON出力のフィールドに名前を付ける方法をエンコーダーに示す注釈としてstructタグを使用します。 これらのJSONエンコードおよびデコードメカニズムは、encoding/jsonpackageにあります。

structタグなしでJSONがどのようにエンコードされるかを確認するには、この例を試してください。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    Name          string
    Password      string
    PreferredFish []string
    CreatedAt     time.Time
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

これにより、次の出力が印刷されます。

Output{
  "Name": "Sammy the Shark",
  "Password": "fisharegreat",
  "CreatedAt": "2019-09-23T15:50:01.203059-04:00"
}

名前、パスワード、ユーザーが作成された時刻などのフィールドを持つユーザーを記述する構造体を定義しました。 main関数内で、PreferredFishを除くすべてのフィールドに値を指定してこのユーザーのインスタンスを作成します(Sammyはすべての魚が好きです)。 次に、Userのインスタンスをjson.MarshalIndent関数に渡しました。 これは、外部のフォーマットツールを使用せずにJSON出力をより簡単に確認できるようにするために使用されます。 この呼び出しをjson.Marshal(u)に置き換えて、空白を追加せずにJSONを受け取ることができます。 json.MarshalIndentへの2つの追加引数は、出力のプレフィックス(空の文字列では省略)と、インデントに使用する文字(ここでは2つのスペース文字)を制御します。 json.MarshalIndentから生成されたエラーはすべてログに記録され、プログラムはos.Exit(1)を使用して終了します。 最後に、json.MarshalIndentから返された[]bytestringにキャストし、結果の文字列をfmt.Printlnに渡して端末に出力します。

構造体のフィールドは、指定したとおりに表示されます。 これは、予想される典型的なJSONスタイルではなく、フィールドの名前にキャメルケースを使用します。 次の例では、フィールドの名前をラクダケーススタイルに合わせて変更します。 この例を実行するとわかるように、目的のフィールド名がエクスポートされたフィールド名に関するGoのルールと競合するため、これは機能しません。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    name          string
    password      string
    preferredFish []string
    createdAt     time.Time
}

func main() {
    u := &User{
        name:      "Sammy the Shark",
        password:  "fisharegreat",
        createdAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

これにより、次の出力が表示されます。

Output{}

このバージョンでは、フィールドの名前をキャメルケースに変更しました。 これで、NamenamePasswordpassword、最後にCreatedAtcreatedAtになります。 mainの本体内で、これらの新しい名前を使用するように構造体のインスタンス化を変更しました。 次に、前と同じように構造体をjson.MarshalIndent関数に渡します。 今回の出力は、空のJSONオブジェクト{}です。

キャメルケーシングフィールドでは、最初の文字を小文字にする必要があります。 JSONはフィールドの名前を気にしませんが、Goはパッケージ外のフィールドの可視性を示すので気にしません。 encoding/jsonパッケージは、使用しているmainパッケージとは別のパッケージであるため、encoding/jsonに表示するには、最初の文字を大文字にする必要があります。 私たちは行き詰まっているように見えます。このフィールドに名前を付けたいことをJSONエンコーダーに伝える方法が必要です。

Structタグを使用してエンコードを制御する

前の例を修正して、各フィールドにstructタグで注釈を付けることにより、キャメルケースのフィールド名で適切にエンコードされたフィールドをエクスポートすることができます。 encoding/jsonが認識する構造体タグには、jsonのキーと、出力を制御する値があります。 キャメルケースバージョンのフィールド名をjsonキーの値として配置することにより、エンコーダーは代わりにその名前を使用します。 この例は、前の2つの試みを修正します。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    Name          string    `json:"name"`
    Password      string    `json:"password"`
    PreferredFish []string  `json:"preferredFish"`
    CreatedAt     time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

これは出力されます:

Output{
  "name": "Sammy the Shark",
  "password": "fisharegreat",
  "preferredFish": null,
  "createdAt": "2019-09-23T18:16:17.57739-04:00"
}

名前の最初の文字を大文字にすることで、フィールド名を他のパッケージに表示されるように変更しました。 ただし、今回はjson:"name"の形式で構造体タグを追加しました。"name"は、構造体をJSONとして出力するときにjson.MarshalIndentが使用する名前です。

JSONが正しくフォーマットされました。 ただし、一部の値のフィールドは、それらの値を設定していなくても印刷されていることに注意してください。 必要に応じて、JSONエンコーダーはこれらのフィールドも削除できます。

空のJSONフィールドを削除する

最も一般的には、JSONで設定されていないフィールドの出力を抑制します。 Goのすべての型には「ゼロ値」があり、それらが設定されているデフォルト値があるため、encoding/jsonパッケージは、このゼロ値を想定するときに一部のフィールドが未設定と見なされる必要があることを通知できるように、追加情報が必要です。 json構造体タグの値部分内で、フィールドの目的の名前に,omitemptyの接尾辞を付けて、フィールドがゼロ値に設定されているときにこのフィールドの出力を抑制するようにJSONエンコーダーに指示できます。 。 次の例は、前の例を修正して空のフィールドを出力しないようにします。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    Name          string    `json:"name"`
    Password      string    `json:"password"`
    PreferredFish []string  `json:"preferredFish,omitempty"`
    CreatedAt     time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

この例は出力します:

Output{
  "name": "Sammy the Shark",
  "password": "fisharegreat",
  "createdAt": "2019-09-23T18:21:53.863846-04:00"
}

前の例を変更して、PreferredFishフィールドに構造体タグjson:"preferredFish,omitempty"が含まれるようにしました。 ,omitempty拡張が存在すると、JSONエンコーダーはそのフィールドをスキップします。これは、フィールドを未設定のままにすることにしたためです。 これは、前の例の出力では値nullでした。

この出力は見た目は格段に優れていますが、ユーザーのパスワードは引き続き印刷されています。 encoding/jsonパッケージは、プライベートフィールドを完全に無視する別の方法を提供します。

プライベートフィールドを無視する

他のパッケージが型と正しく対話できるように、一部のフィールドは構造体からエクスポートする必要があります。 ただし、これらのフィールドの性質は機密性が高い場合があるため、このような状況では、JSONエンコーダーはフィールドが設定されている場合でもフィールドを完全に無視するようにします。 これは、json:構造体タグへの値引数として特別な値-を使用して行われます。

この例は、ユーザーのパスワードを公開する問題を修正します。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "os"
    "time"
)

type User struct {
    Name      string    `json:"name"`
    Password  string    `json:"-"`
    CreatedAt time.Time `json:"createdAt"`
}

func main() {
    u := &User{
        Name:      "Sammy the Shark",
        Password:  "fisharegreat",
        CreatedAt: time.Now(),
    }

    out, err := json.MarshalIndent(u, "", "  ")
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }

    fmt.Println(string(out))
}

この例を実行すると、次の出力が表示されます。

Output{
  "name": "Sammy the Shark",
  "createdAt": "2019-09-23T16:08:21.124481-04:00"
}

この例で前の例から変更したのは、パスワードフィールドがjson:構造体タグに特別な"-"値を使用するようになったことだけです。 この例の出力では、passwordフィールドが存在しなくなっていることがわかります。

encoding/jsonパッケージのこれらの機能、,omitemptyおよび"-"は、標準ではありません。 パッケージがstructタグの値をどうするかは、その実装に依存します。 encoding/jsonパッケージは標準ライブラリの一部であるため、他のパッケージも慣例と同じ方法でこれらの機能を実装しています。 ただし、structタグを使用するサードパーティパッケージのドキュメントを読んで、サポート対象とサポート対象を確認することが重要です。

結論

構造体タグは、構造体で機能するコードの機能を強化する強力な手段を提供します。 多くの標準ライブラリおよびサードパーティパッケージには、structタグを使用して操作をカスタマイズする方法が用意されています。 コードでこれらを効果的に使用すると、このカスタマイズ動作と、これらのフィールドが将来の開発者にどのように使用されるかを簡潔に文書化できます。