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

紹介

構造体、またはstructsは、複数の情報を1つのユニットにまとめるために使用されます。これらの情報のまとまりは、StreetCityState、およびPostalCodeから構成されるAddressなどの高度な概念を記述するために使用されます。データベースやAPIなどのシステムからこの情報を読み取るとき、struct tagsを使用して、この情報が構造体のフィールドにどのように割り当てられるかを制御できます。構造体のフィールドにアタッチされた小さなメタデータのピースである構造体タグは、構造体と連動する他のGoコードに指示を提供します。

構造体タグの形式は?

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

A struct tag looks like this, with the tag offset with backtick ` characters:

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

他のGoコードは、これらの構造体を調べ、リクエストされた特定のキーに割り当てられた値を抽出することができます。構造体タグは、それらを調べる追加のコードなしでは、コードの動作に影響を与えません。

次の例を試して、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)
}

これは次の出力になります:

Output
Hi! My name is Sammy

この例では、User型を定義し、Nameフィールドを持っています。 Nameフィールドにはexample:"name"という構造体タグが付けられています。この特定のタグを会話で「example構造体タグ」と呼びます。なぜなら、それがキーとして「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/json パッケージにあります。

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" }

structを定義し、ユーザーの名前、パスワード、およびユーザーが作成された時間などのフィールドを含むユーザーを表すstructを定義しました。 main関数内で、このユーザーのインスタンスを作成しました。すべてのフィールドに値を指定しましたが、PreferredFish(Sammyはすべての魚が好きです)を除きました。その後、Userのインスタンスをjson.MarshalIndent関数に渡しました。これは、外部のフォーマットツールを使用せずにJSON出力をより簡単に見るために使用されます。この呼び出しは、追加の空白を使用せずにJSONを印刷するためにjson.Marshal(u)で置き換えることができます。 json.MarshalIndentへの2つの追加の引数は、出力へのプレフィックスを制御します(ここでは空の文字列で省略しています)、およびインデントに使用する文字を制御します。ここでは、2つのスペース文字が使用されます。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エンコーダーに、このフィールドをどのように名前付けしたいかを伝える方法が必要です。

エンコードを制御するための構造体タグの使用

前の例を修正して、各フィールドが正しくキャメルケースのフィールド名でエンコードされるように変更できます。各フィールドには、構造体タグを付けることで実現できます。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" }

前の2つの試みとは異なり、構造体のフィールドを他のパッケージから見えるように戻しました。ただし、今回はjson:"name"という形式の構造体タグを追加しました。ここで"name"は、json.MarshalIndentが構造体をJSONとして出力する際に使用する名前です。

これで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"-"、および その他のオプション — は標準ではありません。構造体タグの値に関してパッケージが何をするかは、その実装に依存します。標準ライブラリの一部である encoding/json パッケージは、他のパッケージが同じようにこれらの機能を実装することを慣例として採用しています。ただし、構造体タグを使用するサードパーティパッケージのドキュメントを読むことは重要です。何がサポートされているか、何がされていないかを学ぶために。

結論

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

Source:
https://www.digitalocean.com/community/tutorials/how-to-use-struct-tags-in-go