はじめに
JSON (JavaScript Object Notation) は、Web APIや設定ファイルなど、様々な場面でデータを交換するために広く使われている軽量なデータ形式です。 Go言語には、このJSONデータを簡単に扱うための強力な標準パッケージ encoding/json
が用意されています。
このパッケージを使えば、Goのデータ構造(構造体やマップなど)とJSON文字列を相互に変換することができます。 このブログ記事では、encoding/json
パッケージの基本的な使い方を学んでいきましょう!
Goのデータ構造からJSONへのエンコード (Marshal)
Goのデータ構造(例えば、構造体やマップ)をJSON形式のバイト列([]byte
)に変換することを「マーシャリング (Marshalling)」または「エンコード (Encoding)」と呼びます。 これには json.Marshal
関数を使用します。
構造体をJSONに変換する例を見てみましょう。
package main
import ( "encoding/json" "fmt" "log"
)
type User struct { ID int Name string Age int
}
func main() { user := User{ID: 1, Name: "Gopher", Age: 10} // 構造体をJSONバイト列にマーシャリング jsonData, err := json.Marshal(user) if err != nil { log.Fatalf("JSONマーシャリングエラー: %s", err) } // バイト列を文字列として出力 fmt.Println(string(jsonData)) // 出力: {"ID":1,"Name":"Gopher","Age":10}
}
json.Marshal
は、Goのデータ構造を受け取り、JSON形式のバイト列とエラーを返します。 Goの構造体のフィールド名は、デフォルトではそのままJSONのキー名になります。 ただし、JSONとしてエンコードされるのはエクスポートされたフィールド(大文字で始まるフィールド)のみです。
フィールドタグでJSON出力をカスタマイズ
JSONのキー名をGoのフィールド名と違う名前にしたい場合や、特定の条件下でフィールドを省略したい場合があります。 このような場合、構造体のフィールドに「タグ (Tag)」を付与することで、エンコード時の挙動をカスタマイズできます。
タグはバッククォート(`
)で囲み、json:"..."
の形式で指定します。
package main
import ( "encoding/json" "fmt" "log"
)
type Product struct { ProductID int `json:"product_id"` // JSONキー名を "product_id" にする Name string `json:"name"` // JSONキー名を "name" にする Description string `json:"description,omitempty"` // 値が空ならJSONに含めない Stock int `json:"-"` // このフィールドはJSONに含めない
}
func main() { product1 := Product{ProductID: 101, Name: "Go T-Shirt", Description: "Official Go Gopher T-shirt", Stock: 50} jsonData1, _ := json.Marshal(product1) fmt.Println(string(jsonData1)) // 出力: {"product_id":101,"name":"Go T-Shirt","description":"Official Go Gopher T-shirt"} product2 := Product{ProductID: 102, Name: "Go Sticker", Stock: 100} // Descriptionは空 jsonData2, _ := json.Marshal(product2) fmt.Println(string(jsonData2)) // 出力: {"product_id":102,"name":"Go Sticker"} (descriptionはomitemptyにより省略)
}
よく使われるタグオプション:
json:"キー名"
: JSONでのキー名を指定します。json:",omitempty"
: フィールドの値がGoのゼロ値(数値なら0, 文字列なら””, ポインタならnilなど)の場合、そのフィールドをJSON出力に含めません。json:"-"
: このフィールドをJSONのエンコード・デコード対象から完全に除外します。json:"キー名,omitempty"
: キー名を指定し、かつ値がゼロ値なら省略します。
マップやスライスも同様に json.Marshal
でJSONに変換できます。
package main
import ( "encoding/json" "fmt"
)
func main() { // スライスのマーシャリング langs := []string{"Go", "Python", "JavaScript"} langsJson, _ := json.Marshal(langs) fmt.Println(string(langsJson)) // 出力: ["Go","Python","JavaScript"] // マップのマーシャリング counts := map[string]int{"apple": 5, "banana": 10} countsJson, _ := json.Marshal(counts) fmt.Println(string(countsJson)) // 出力: {"apple":5,"banana":10}
}
JSONからGoのデータ構造へのデコード (Unmarshal)
JSON形式のバイト列をGoのデータ構造(構造体やマップなど)に変換することを「アンマーシャリング (Unmarshalling)」または「デコード (Decoding)」と呼びます。 これには json.Unmarshal
関数を使用します。
package main
import ( "encoding/json" "fmt" "log"
)
type Point struct { X int `json:"x"` Y int `json:"y"`
}
func main() { jsonInput := []byte(`{"x":10, "y":20}`) var p Point // JSONバイト列をPoint構造体にアンマーシャリング // 第2引数には、変換結果を格納する変数へのポインタを渡す err := json.Unmarshal(jsonInput, &p) if err != nil { log.Fatalf("JSONアンマーシャリングエラー: %s", err) } fmt.Printf("Point: X=%d, Y=%d\n", p.X, p.Y) // 出力: Point: X=10, Y=20
}
json.Unmarshal
は、JSONバイト列と、変換結果を格納する変数へのポインタを受け取ります。 JSONのキーとGoの構造体フィールドのマッピングは、Marshal
と同様にタグを考慮して行われます。 大文字・小文字の違いは基本的に無視されますが、明確にするためにタグを使うことが推奨されます。
JSONの構造が事前にわからない場合や、柔軟に扱いたい場合は、map[string]interface{}
を使うこともできます。 interface{}
(またはエイリアスの any
) は、任意の型の値を保持できます。
package main
import ( "encoding/json" "fmt" "log"
)
func main() { jsonInput := []byte(`{"name": "Gopher", "active": true, "details": {"age": 10, "projects": ["Go", "Web"]}}`) var data map[string]interface{} // any でも可 err := json.Unmarshal(jsonInput, &data) if err != nil { log.Fatalf("JSONアンマーシャリングエラー: %s", err) } // 値にアクセスするには型アサーションが必要 name := data["name"].(string) active := data["active"].(bool) details := data["details"].(map[string]interface{}) age := details["age"].(float64) // JSONの数値はデフォルトで float64 になることに注意 fmt.Printf("Name: %s, Active: %t, Age: %f\n", name, active, age) // 出力: Name: Gopher, Active: true, Age: 10.000000
}
interface{}
を使うと柔軟ですが、値を利用する際に型アサーションが必要になり、型安全性が低下する点に注意が必要です。 JSONの数値は、Goではデフォルトで float64
としてデコードされます。
ストリーム処理 (Encoder/Decoder)
ファイルやネットワーク接続など、データをストリームとして扱う場合は、json.Encoder
と json.Decoder
を使うのが便利です。 これらは io.Writer
や io.Reader
インターフェースと連携します。
Encoder
は、GoのデータをJSON形式で io.Writer
(ファイル、HTTPレスポンスなど)に書き込みます。
package main
import ( "encoding/json" "os"
)
type Config struct { Server string `json:"server"` Port int `json:"port"`
}
func main() { cfg := Config{Server: "localhost", Port: 8080} // 標準出力 (os.Stdout) に書き込むEncoderを作成 encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") // 人が読みやすいようにインデントを設定 // 構造体をJSONとしてエンコードして書き込む err := encoder.Encode(cfg) if err != nil { log.Println("エンコードエラー:", err) } // 出力: // { // "server": "localhost", // "port": 8080 // }
}
Decoder
は、io.Reader
(ファイル、HTTPリクエストボディなど)からJSONデータを読み取り、Goのデータ構造にデコードします。 大きなJSONデータや、連続するJSONオブジェクトを効率的に処理するのに役立ちます。
package main
import ( "encoding/json" "fmt" "log" "strings"
)
type Message struct { User string `json:"user"` Text string `json:"text"`
}
func main() { // 連続するJSONオブジェクトを含む文字列 jsonStream := ` {"user": "Alice", "text": "Hello!"} {"user": "Bob", "text": "Hi Alice!"} ` reader := strings.NewReader(jsonStream) // readerから読み取るDecoderを作成 decoder := json.NewDecoder(reader) // ストリームが終わるまでJSONオブジェクトをデコード for decoder.More() { var msg Message err := decoder.Decode(&msg) if err != nil { log.Fatal("デコードエラー:", err) } fmt.Printf("User: %s, Text: %s\n", msg.User, msg.Text) } // 出力: // User: Alice, Text: Hello! // User: Bob, Text: Hi Alice!
}
Marshal
/Unmarshal
はデータ全体をメモリに読み込むため、非常に大きなJSONデータを扱う場合は Encoder
/Decoder
を使う方がメモリ効率が良い場合があります。
よくあるパターンと注意点
- キー名とフィールド名のマッピング: JSONでよく使われる
snake_case
やcamelCase
のキー名を、GoのPascalCase
(orCamelCase
) のフィールド名に対応させるには、構造体タグjson:"..."
を使いましょう。 - 数値型: JSONの数値は、Goではデフォルトで
float64
にデコードされます。整数として扱いたい場合は、デコード先の構造体のフィールドをint
などにしておくか、json.Number
型を使って後で変換します。package main import ( "encoding/json" "fmt" "log" ) func main() { jsonInput := []byte(`{"value": 123}`) var data map[string]json.Number // json.Numberを使う err := json.Unmarshal(jsonInput, &data) if err != nil { log.Fatal(err) } // json.Numberからint64に変換 intValue, err := data["value"].Int64() if err != nil { log.Fatal(err) } fmt.Println("Int value:", intValue) // 出力: Int value: 123 // json.Numberからfloat64に変換 floatValue, err := data["value"].Float64() if err != nil { log.Fatal(err) } fmt.Println("Float value:", floatValue) // 出力: Float value: 123 }
- Null値: JSONの
null
は、Goのポインタ型 (*string
,*int
など) やinterface{}
,map
,slice
のnil
に対応します。値型(string
,int
など)のフィールドにnull
をデコードしようとするとエラーになることがあります。ポインタ型を使うか、omitempty
タグを検討しましょう。 - エラーハンドリング:
Marshal
やUnmarshal
,Encode
,Decode
はエラーを返す可能性があります。不正なJSONデータや型変換のエラーなど、必ずエラーチェックを行いましょう。 - 未知のキー: デフォルトでは、
Unmarshal
は構造体に定義されていないJSONキーを無視します。未知のキーが存在する場合にエラーとしたい場合は、Decoder
のDisallowUnknownFields()
メソッドを使用します。package main import ( "encoding/json" "fmt" "log" "strings" ) type Simple struct { Key string `json:"key"` } func main() { jsonInput := `{"key": "value", "unknown_key": "ignored?"}` reader := strings.NewReader(jsonInput) decoder := json.NewDecoder(reader) decoder.DisallowUnknownFields() // 未知のフィールドをエラーにする設定 var s Simple err := decoder.Decode(&s) if err != nil { // ここでエラーが発生する log.Printf("デコードエラー (未知のフィールドあり): %v", err) } else { fmt.Printf("Decoded: %+v\n", s) } }
まとめ
Goの encoding/json
パッケージを使うことで、JSONデータのエンコード(マーシャリング)とデコード(アンマーシャリング)を簡単に行うことができます。
json.Marshal
: Goのデータ構造をJSONバイト列に変換json.Unmarshal
: JSONバイト列をGoのデータ構造に変換- 構造体タグ: JSONキー名のカスタマイズ、フィールドの省略、除外などに使用
json.Encoder
/json.Decoder
: ファイルやネットワークなどのストリーム処理に便利
Web APIとの連携や設定ファイルの読み書きなど、JSONは多くの場面で活用されます。 このパッケージの使い方をマスターして、Goでの開発をさらに効率的に進めましょう!
次は、テンプレート処理(html/template)に進んで、動的なHTML生成について学びましょう!