title: 【転載】あなたが知っておくべき Go 言語の JSON テクニック
date: 2021-08-09 16:33:33
comment: false
toc: true
category:
- Golang
tags: - 转载
- Go
- Json
この記事は、あなたが知っておくべき Go 言語の JSON テクニック | 李文周のブログから転載されています。
この記事では、私がプロジェクトで遭遇した Go 言語の JSON データと構造体の相互変換に関する問題とその解決策をまとめています。
基本的なシリアル化#
まず、Go 言語におけるjson.Marshal()
(シリアル化)とjson.Unmarshal
(デシリアル化)の基本的な使い方を見てみましょう。
type Person struct {
Name string
Age int64
Weight float64
}
func main() {
p1 := Person{
Name: "七米",
Age: 18,
Weight: 71.5,
}
// struct -> json string
b, err := json.Marshal(p1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// json string -> struct
var p2 Person
err = json.Unmarshal(b, &p2)
if err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("p2:%#v\n", p2)
}
出力:
str:{"Name":"七米","Age":18,"Weight":71.5}
p2:main.Person{Name:"七米", Age:18, Weight:71.5}
構造体タグの紹介#
Tag
は構造体のメタ情報であり、実行時にリフレクションを通じて読み取ることができます。 Tag
は構造体フィールドの後ろに定義され、一対のバッククォートで囲まれています。具体的なフォーマットは以下の通りです:
`key1:"value1" key2:"value2"`
構造体タグは 1 つ以上のキーと値のペアで構成されています。キーと値はコロンで区切られ、値はダブルクォートで囲まれます。同じ構造体フィールドに複数のキーと値のペアタグを設定することができ、異なるキーと値のペアはスペースで区切られます。
JSON タグを使用してフィールド名を指定する#
シリアル化とデシリアル化はデフォルトで構造体のフィールド名を使用しますが、構造体フィールドにタグを追加することで JSON シリアル化で生成されるフィールド名を指定できます。
// JSONタグを使用してシリアル化とデシリアル化の動作を指定
type Person struct {
Name string `json:"name"` // JSONシリアル化/デシリアル化時に小文字のnameを使用
Age int64
Weight float64
}
特定のフィールドを無視する#
JSON シリアル化 / デシリアル化の際に構造体の特定のフィールドを無視したい場合は、タグに-
を追加することで実現できます。
// JSONタグを使用してJSONシリアル化とデシリアル化の動作を指定
type Person struct {
Name string `json:"name"` // JSONシリアル化/デシリアル化時に小文字のnameを使用
Age int64
Weight float64 `json:"-"` // JSONシリアル化/デシリアル化時にこのフィールドを無視
}
空の値のフィールドを無視する#
構造体のフィールドに値がない場合、json.Marshal()
シリアル化時にはこれらのフィールドを無視せず、デフォルトでフィールドの型のゼロ値(例えば、int
とfloat
の型のゼロ値は 0、string
の型のゼロ値は""
、オブジェクト型のゼロ値は nil)を出力します。シリアル化時にこれらの値のないフィールドを無視したい場合は、対応するフィールドにomitempty
タグを追加できます。
例を挙げると:
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Hobby []string `json:"hobby"`
}
func omitemptyDemo() {
u1 := User{
Name: "七米",
}
// struct -> json string
b, err := json.Marshal(u1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
}
出力結果:
str:{"name":"七米","email":"","hobby":null}
最終的なシリアル化結果から空の値のフィールドを削除したい場合は、以下のように構造体を定義できます:
// タグにomitemptyを追加して空の値を無視
// 注意:ここでhobby,omitemptyはjsonタグの値であり、間にカンマで区切られています
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
}
この時、再度上記のomitemptyDemo
を実行すると、出力結果は以下のようになります:
str:{"name":"七米"} // シリアル化結果にemailとhobbyフィールドはありません
ネストされた構造体の空の値のフィールドを無視する#
まず、いくつかの構造体のネストの例を見てみましょう:
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
Profile
}
type Profile struct {
Website string `json:"site"`
Slogan string `json:"slogan"`
}
func nestedStructDemo() {
u1 := User{
Name: "七米",
Hobby: []string{"サッカー", "双色球"},
}
b, err := json.Marshal(u1)
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
}
匿名ネストされたProfile
のシリアル化後の JSON 文字列は単層です:
str:{"name":"七米","hobby":["サッカー","双色球"],"site":"","slogan":""}
ネストされた JSON 文字列にするには、具名ネストまたはフィールドタグを定義する必要があります:
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
Profile `json:"profile"`
}
// str:{"name":"七米","hobby":["サッカー","双色球"],"profile":{"site":"","slogan":""}}
ネストされた構造体が空の値の時に、そのフィールドを無視したい場合、omitempty
を追加するだけでは不十分です:
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
Profile `json:"profile,omitempty"`
}
// str:{"name":"七米","hobby":["サッカー","双色球"],"profile":{"site":"","slogan":""}}
ネストされた構造体のポインタを使用する必要があります:
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Hobby []string `json:"hobby,omitempty"`
*Profile `json:"profile,omitempty"`
}
// str:{"name":"七米","hobby":["サッカー","双色球"]}
元の構造体を変更せずに空の値のフィールドを無視する#
User
を JSON シリアル化する必要がありますが、パスワードもシリアル化したくない場合、User
構造体を変更したくないときは、別の構造体PublicUser
を作成し、元のUser
を匿名ネストし、同時にPassword
フィールドを匿名構造体ポインタ型として指定し、omitempty
タグを追加します。以下はサンプルコードです:
type User struct {
Name string `json:"name"`
Password string `json:"password"`
}
type PublicUser struct {
*User // 匿名ネスト
Password *struct{} `json:"password,omitempty"`
}
func omitPasswordDemo() {
u1 := User{
Name: "七米",
Password: "123456",
}
b, err := json.Marshal(PublicUser{User: &u1})
if err != nil {
fmt.Printf("json.Marshal u1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b) // str:{"name":"七米"}
}
文字列形式の数字を優雅に処理する#
時には、フロントエンドから送信される JSON データに文字列型の数字が使用されることがあります。この場合、構造体タグにstring
を追加して、JSON パッケージに文字列から対応するフィールドのデータを解析させることができます:
type Card struct {
ID int64 `json:"id,string"` // stringタグを追加
Score float64 `json:"score,string"` // stringタグを追加
}
func intAndStringDemo() {
jsonStr1 := `{"id": "1234567","score": "88.50"}`
var c1 Card
if err := json.Unmarshal([]byte(jsonStr1), &c1); err != nil {
fmt.Printf("json.Unmarshal jsonStr1 failed, err:%v\n", err)
return
}
fmt.Printf("c1:%#v\n", c1) // c1:main.Card{ID:1234567, Score:88.5}
}
整数を浮動小数点数に変換する#
JSON プロトコルには整数型と浮動小数点型の区別はなく、両者は一般に number と呼ばれます。JSON 文字列内の数字は、Go 言語の JSON パッケージでデシリアル化された後、すべてfloat64
型になります。以下のコードはこの問題を示しています:
func jsonDemo() {
// map[string]interface{} -> json string
var m = make(map[string]interface{}, 1)
m["count"] = 1 // int
b, err := json.Marshal(m)
if err != nil {
fmt.Printf("marshal failed, err:%v\n", err)
}
fmt.Printf("str:%#v\n", string(b))
// json string -> map[string]interface{}
var m2 map[string]interface{}
err = json.Unmarshal(b, &m2)
if err != nil {
fmt.Printf("unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("value:%v\n", m2["count"]) // 1
fmt.Printf("type:%T\n", m2["count"]) // float64
}
このようなシーンでは、数字をより適切に処理するためにdecoder
を使用してデシリアル化する必要があります。サンプルコードは以下の通りです:
func decoderDemo() {
// map[string]interface{} -> json string
var m = make(map[string]interface{}, 1)
m["count"] = 1 // int
b, err := json.Marshal(m)
if err != nil {
fmt.Printf("marshal failed, err:%v\n", err)
}
fmt.Printf("str:%#v\n", string(b))
// json string -> map[string]interface{}
var m2 map[string]interface{}
// decoder方式でデシリアル化し、number型を指定
decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
err = decoder.Decode(&m2)
if err != nil {
fmt.Printf("unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("value:%v\n", m2["count"]) // 1
fmt.Printf("type:%T\n", m2["count"]) // json.Number
// m2["count"]をjson.Numberに変換した後、Int64()メソッドを呼び出してint64型の値を取得
count, err := m2["count"].(json.Number).Int64()
if err != nil {
fmt.Printf("parse to int64 failed, err:%v\n", err)
return
}
fmt.Printf("type:%T\n", int(count)) // int
}
json.Number
のソースコード定義は以下の通りです:
// A Number represents a JSON number literal.
type Number string
// String returns the literal text of the number.
func (n Number) String() string { return string(n) }
// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
return strconv.ParseFloat(string(n), 64)
}
// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
return strconv.ParseInt(string(n), 10, 64)
}
私たちは number 型の JSON フィールドを処理する際に、最初にjson.Number
型を取得し、そのフィールドの実際の型に基づいてFloat64()
またはInt64()
を呼び出す必要があります。
時間フィールドのカスタム解析#
Go 言語の組み込み JSON パッケージは、RFC3339
標準で定義された時間フォーマットを使用しており、時間フィールドをシリアル化する際に多くの制限があります。
type Post struct {
CreateTime time.Time `json:"create_time"`
}
func timeFieldDemo() {
p1 := Post{CreateTime: time.Now()}
b, err := json.Marshal(p1)
if err != nil {
fmt.Printf("json.Marshal p1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
jsonStr := `{"create_time":"2020-04-05 12:25:42"}`
var p2 Post
if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("p2:%#v\n", p2)
}
上記のコードの出力結果は以下の通りです:
str:{"create_time":"2020-04-05T12:28:06.799214+08:00"}
json.Unmarshal failed, err:parsing time ""2020-04-05 12:25:42"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 12:25:42"" as "T"
つまり、組み込みの JSON パッケージは、一般的に使用される文字列時間フォーマット(例えば、2020-04-05 12:25:42
)を認識しません。
しかし、json.Marshaler
/json.Unmarshaler
インターフェースを実装することで、カスタムのイベントフォーマット解析を実現できます。
type CustomTime struct {
time.Time
}
const ctLayout = "2006-01-02 15:04:05"
var nilTime = (time.Time{}).UnixNano()
func (ct *CustomTime) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), "\"")
if s == "null" {
ct.Time = time.Time{}
return
}
ct.Time, err = time.Parse(ctLayout, s)
return
}
func (ct *CustomTime) MarshalJSON() ([]byte, error) {
if ct.Time.UnixNano() == nilTime {
return []byte("null"), nil
}
return []byte(fmt.Sprintf("\"%s\"", ct.Time.Format(ctLayout))), nil
}
func (ct *CustomTime) IsSet() bool {
return ct.UnixNano() != nilTime
}
type Post struct {
CreateTime CustomTime `json:"create_time"`
}
func timeFieldDemo() {
p1 := Post{CreateTime: CustomTime{time.Now()}}
b, err := json.Marshal(p1)
if err != nil {
fmt.Printf("json.Marshal p1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
jsonStr := `{"create_time":"2020-04-05 12:25:42"}`
var p2 Post
if err := json.Unmarshal([]byte(jsonStr), &p2); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("p2:%#v\n", p2)
}
MarshalJSON と UnmarshalJSON メソッドのカスタマイズ#
上記のカスタムタイプの方法は少し冗長ですが、次に比較的便利な方法を見てみましょう。
まず知っておくべきことは、特定のタイプに対してMarshalJSON()([]byte, error)
とUnmarshalJSON(b []byte) error
メソッドを実装できれば、そのタイプはシリアル化(MarshalJSON)/ デシリアル化(UnmarshalJSON)時にあなたのカスタムメソッドを使用します。
type Order struct {
ID int `json:"id"`
Title string `json:"title"`
CreatedTime time.Time `json:"created_time"`
}
const layout = "2006-01-02 15:04:05"
// MarshalJSONはOrderタイプにカスタムのMarshalJSONメソッドを実装します
func (o *Order) MarshalJSON() ([]byte, error) {
type TempOrder Order // Orderフィールドと一致する新しいタイプを定義
return json.Marshal(struct {
CreatedTime string `json:"created_time"`
*TempOrder // 直接Orderをネストして無限ループを避ける
}{
CreatedTime: o.CreatedTime.Format(layout),
TempOrder: (*TempOrder)(o),
})
}
// UnmarshalJSONはOrderタイプにカスタムのUnmarshalJSONメソッドを実装します
func (o *Order) UnmarshalJSON(data []byte) error {
type TempOrder Order // Orderフィールドと一致する新しいタイプを定義
ot := struct {
CreatedTime string `json:"created_time"`
*TempOrder // 直接Orderをネストして無限ループを避ける
}{
TempOrder: (*TempOrder)(o),
}
if err := json.Unmarshal(data, &ot); err != nil {
return err
}
var err error
o.CreatedTime, err = time.Parse(layout, ot.CreatedTime)
if err != nil {
return err
}
return nil
}
// カスタムシリアル化メソッド
func customMethodDemo() {
o1 := Order{
ID: 123456,
Title: "《七米のGo学習ノート》",
CreatedTime: time.Now(),
}
// カスタムのMarshalJSONメソッドを通じてstruct -> json stringを実現
b, err := json.Marshal(&o1)
if err != nil {
fmt.Printf("json.Marshal o1 failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// カスタムのUnmarshalJSONメソッドを通じてjson string -> structを実現
jsonStr := `{"created_time":"2020-04-05 10:18:20","id":123456,"title":"《七米のGo学習ノート》"}`
var o2 Order
if err := json.Unmarshal([]byte(jsonStr), &o2); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("o2:%#v\n", o2)
}
出力結果:
str:{"created_time":"2020-04-05 10:32:20","id":123456,"title":"《七米のGo学習ノート》"}
o2:main.Order{ID:123456, Title:"《七米のGo学習ノート》", CreatedTime:time.Time{wall:0x0, ext:63721678700, loc:(*time.Location)(nil)}}
匿名構造体を使用してフィールドを追加する#
内蔵構造体を使用すると構造体のフィールドを拡張できますが、時には新しい構造体を単独で定義する必要がない場合もあります。匿名構造体を使用して操作を簡素化できます:
type UserInfo struct {
ID int `json:"id"`
Name string `json:"name"`
}
func anonymousStructDemo() {
u1 := UserInfo{
ID: 123456,
Name: "七米",
}
// 匿名構造体を使用してUserを内蔵し、追加フィールドTokenを追加
b, err := json.Marshal(struct {
*UserInfo
Token string `json:"token"`
}{
&u1,
"91je3a4s72d1da96h",
})
if err != nil {
fmt.Printf("json.Marsha failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// str:{"id":123456,"name":"七米","token":"91je3a4s72d1da96h"}
}
匿名構造体を使用して複数の構造体を組み合わせる#
同様に、匿名構造体を使用して複数の構造体を組み合わせてデータをシリアル化およびデシリアル化することもできます:
type Comment struct {
Content string
}
type Image struct {
Title string `json:"title"`
URL string `json:"url"`
}
func anonymousStructDemo2() {
c1 := Comment{
Content: "自分を過大評価しないでください",
}
i1 := Image{
Title: "感謝コード",
URL: "https://www.liwenzhou.com/images/zanshang_qr.jpg",
}
// struct -> json string
b, err := json.Marshal(struct {
*Comment
*Image
}{&c1, &i1})
if err != nil {
fmt.Printf("json.Marshal failed, err:%v\n", err)
return
}
fmt.Printf("str:%s\n", b)
// json string -> struct
jsonStr := `{"Content":"自分を過大評価しないでください","title":"感謝コード","url":"https://www.liwenzhou.com/images/zanshang_qr.jpg"}`
var (
c2 Comment
i2 Image
)
if err := json.Unmarshal([]byte(jsonStr), &struct {
*Comment
*Image
}{&c2, &i2}); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("c2:%#v i2:%#v\n", c2, i2)
}
出力:
str:{"Content":"自分を過大評価しないでください","title":"感謝コード","url":"https://www.liwenzhou.com/images/zanshang_qr.jpg"}
c2:main.Comment{Content:"自分を過大評価しないでください"} i2:main.Image{Title:"感謝コード", URL:"https://www.liwenzhou.com/images/zanshang_qr.jpg"}
不確定な階層の JSON を処理する#
もし JSON 文字列が固定のフォーマットを持たず、対応する構造体を定義するのが難しい場合、json.RawMessage
を使用して生のバイトデータを保存できます。
type sendMsg struct {
User string `json:"user"`
Msg string `json:"msg"`
}
func rawMessageDemo() {
jsonStr := `{"sendMsg":{"user":"q1mi","msg":"自分を過大評価しないでください"},"say":"Hello"}`
// 値の型をjson.RawMessageとするマップを定義し、後でより柔軟に処理できるようにする
var data map[string]json.RawMessage
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
fmt.Printf("json.Unmarshal jsonStr failed, err:%v\n", err)
return
}
var msg sendMsg
if err := json.Unmarshal(data["sendMsg"], &msg); err != nil {
fmt.Printf("json.Unmarshal failed, err:%v\n", err)
return
}
fmt.Printf("msg:%#v\n", msg)
// msg:main.sendMsg{User:"q1mi", Msg:"自分を過大評価しないでください"}
}
参考リンク:
https://stackoverflow.com/questions/25087960/json-unmarshal-time-that-isnt-in-rfc-3339-format
https://colobu.com/2017/06/21/json-tricks-in-Go/
https://stackoverflow.com/questions/11066946/partly-json-unmarshal-into-a-map-in-go