JSON処理
レッスン21:JSON処理
生活での例え
あなたが翻訳者であると想像してください。JSONは世界で最も広く使われている「共通言語」です。Goプログラムが他のシステム(フロントエンド、API、データベース)と通信する必要がある場合、以下を行う必要があります:
- エンコード(Marshal):Goの構造体をJSON形式に「翻訳」して送信する
- デコード(Unmarshal):受信したJSONをGoの構造体に「翻訳」して使用する
翻訳者が両方の言語のルールと慣用句を理解する必要があるように、Goのencoding/jsonパッケージはJSONを処理するための強力なツールです。
コアコンセプト
| コンセプト | 説明 |
|---|---|
| シリアライゼーション(Marshal) | Goのデータ構造をJSONバイトスライスに変換 |
| デシリアライゼーション(Unmarshal) | JSONバイトスライスをGoのデータ構造に解析 |
| 構造体タグ | JSONフィールド名と動作を制御するメタデータ |
| ストリーミング | Decoder/Encoderを使用して大量データやネットワークストリームを処理 |
| カスタムシリアライゼーション | Marshaler/Unmarshalerインターフェースを実装してカスタム変換ロジックを実現 |
基本構文と使い方
1. パッケージのインポート
GO
import "encoding/json"
2. シリアライゼーション:構造体 → JSON
GO
// 構造体を定義
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 28, Email: "alice@example.com"}
// シリアライズ
data, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// 出力: {"Name":"Alice","Age":28,"Email":"alice@example.com"}
💡 ヒント:
json.Marshalは[]byteを返すため、読みやすいJSONを表示するにはstring()変換が必要です。
3. デシリアライゼーション:JSON → 構造体
GO
jsonStr := `{"Name":"Bob","Age":32,"Email":"bob@example.com"}`
var user User
err := json.Unmarshal([]byte(jsonStr), &user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("名前: %s, 年齢: %d\n", user.Name, user.Age)
// 出力: 名前: Bob, 年齢: 32
💡 ヒント:
Unmarshalの第2パラメータはポインタでなければなりません。さもないと変更が反映されません。
4. 構造体タグ
GO
type Product struct {
ID int `json:"id"` // JSONフィールド名を指定
Name string `json:"name"` // 小文字命名はJSONにより親和性が高い
Price float64 `json:"price"`
Desc string `json:"description,omitempty"` // 空の場合は省略
internal string `json:"-"` // このフィールドを完全に無視
}
💡 ヒント:
omitempty:フィールドがゼロ値の場合、JSON出力から省略されます-:このフィールドはJSONに出現しません- タグ名はフィールド名より優先されます
5. 一般的な型マッピング
| Goの型 | JSONの型 |
|---|---|
string |
string |
int, float64 |
number |
bool |
boolean |
nil |
null |
[]T |
array |
map[string]T |
object |
struct |
object |
例題
例:基本的なJSONシリアライゼーションとデシリアライゼーション(難易度⭐)
GO
package main
import (
"encoding/json"
"fmt"
"log"
)
// Book構造体
type Book struct {
Title string `json:"title"`
Author string `json:"author"`
Pages int `json:"pages"`
Tags []string `json:"tags"`
InStock bool `json:"in_stock"`
}
func main() {
// === シリアライゼーション ===
book := Book{
Title: "Go in Action",
Author: "John Smith",
Pages: 350,
Tags: []string{"Programming", "Go", "Backend"},
InStock: true,
}
// プリティプリント(インデント付き)
jsonData, err := json.MarshalIndent(book, "", " ")
if err != nil {
log.Fatal("シリアライゼーション失敗:", err)
}
fmt.Println("=== シリアライゼーション結果 ===")
fmt.Println(string(jsonData))
// === デシリアライゼーション ===
jsonStr := `{
"title": "Mastering Go",
"author": "Jane Doe",
"pages": 480,
"tags": ["Go", "Advanced", "Concurrency"],
"in_stock": false
}`
var newBook Book
err = json.Unmarshal([]byte(jsonStr), &newBook)
if err != nil {
log.Fatal("デシリアライゼーション失敗:", err)
}
fmt.Println("\n=== デシリアライゼーション結果 ===")
fmt.Printf("タイトル: %s\n", newBook.Title)
fmt.Printf("著者: %s\n", newBook.Author)
fmt.Printf("タグ: %v\n", newBook.Tags)
fmt.Printf("在庫あり: %v\n", newBook.InStock)
}
出力:
TEXT
=== シリアライゼーション結果 ===
{
"title": "Go in Action",
"author": "John Smith",
"pages": 350,
"tags": [
"Programming",
"Go",
"Backend"
],
"in_stock": true
}
=== デシリアライゼーション結果 ===
タイトル: Mastering Go
著者: Jane Doe
タグ: [Go Advanced Concurrency]
在庫あり: false
例:ネストされたJSONとMapの処理(難易度⭐⭐)
GO
package main
import (
"encoding/json"
"fmt"
"log"
)
// Address構造体
type Address struct {
City string `json:"city"`
Street string `json:"street"`
ZipCode string `json:"zip_code"`
}
// 連絡先情報
type Contact struct {
Phone string `json:"phone"`
Email string `json:"email"`
}
// Employee構造体(ネスト付き)
type Employee struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"` // ネストされた構造体
Contact Contact `json:"contact"` // ネストされた構造体
Skills []string `json:"skills"` // スライス
Metadata map[string]string `json:"metadata"` // 動的フィールド
}
func main() {
// ネストされたデータを構築
emp := Employee{
Name: "Alice",
Age: 35,
Address: Address{
City: "Beijing",
Street: "88 Jianguo Road, Chaoyang District",
ZipCode: "100022",
},
Contact: Contact{
Phone: "13800138000",
Email: "alice@example.com",
},
Skills: []string{"Go", "Python", "Docker"},
Metadata: map[string]string{
"department": "Engineering",
"level": "P7",
"joined": "2020-03-15",
},
}
// シリアライズ
data, err := json.MarshalIndent(emp, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println("=== ネストされたJSONシリアライゼーション ===")
fmt.Println(string(data))
// 動的JSONの処理(mapを使用)
dynamicJSON := `{
"event": "user_login",
"timestamp": 1700000000,
"data": {
"user_id": 12345,
"ip": "192.168.1.100",
"browser": "Chrome"
},
"tags": ["web", "auth"]
}`
var result map[string]interface{}
err = json.Unmarshal([]byte(dynamicJSON), &result)
if err != nil {
log.Fatal(err)
}
fmt.Println("\n=== 動的JSON解析 ===")
fmt.Printf("イベント: %s\n", result["event"])
fmt.Printf("タイムスタンプ: %.0f\n", result["timestamp"])
// ネストされたmapにアクセス
if data, ok := result["data"].(map[string]interface{}); ok {
fmt.Printf("ユーザーID: %.0f\n", data["user_id"])
fmt.Printf("IPアドレス: %s\n", data["ip"])
}
// 配列にアクセス
if tags, ok := result["tags"].([]interface{}); ok {
fmt.Print("タグ: ")
for _, tag := range tags {
fmt.Printf("%s ", tag)
}
fmt.Println()
}
}
出力:
TEXT
=== ネストされたJSONシリアライゼーション ===
{
"name": "Alice",
"age": 35,
"address": {
"city": "Beijing",
"street": "88 Jianguo Road, Chaoyang District",
"zip_code": "100022"
},
"contact": {
"phone": "13800138000",
"email": "alice@example.com"
},
"skills": [
"Go",
"Python",
"Docker"
],
"metadata": {
"department": "Engineering",
"joined": "2020-03-15",
"level": "P7"
}
}
=== 動的JSON解析 ===
イベント: user_login
タイムスタンプ: 1700000000
ユーザーID: 12345
IPアドレス: 192.168.1.100
タグ: web auth
例:カスタムシリアライゼーションとストリーミング(難易度⭐⭐⭐)
GO
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
)
// CustomTime カスタム時間型
type CustomTime struct {
time.Time
}
// json.Marshalerインターフェースを実装
func (ct CustomTime) MarshalJSON() ([]byte, error) {
// 出力フォーマット: 2006-01-02 15:04:05
formatted := ct.Format("2006-01-02 15:04:05")
return json.Marshal(formatted)
}
// json.Unmarshalerインターフェースを実装
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
// 複数のフォーマット解析をサポート
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02T15:04:05",
"2006/01/02",
}
for _, format := range formats {
t, err := time.Parse(format, s)
if err == nil {
ct.Time = t
return nil
}
}
return fmt.Errorf("時間を解析できませんでした: %s", s)
}
// Status カスタム列挙型
type Status int
const (
StatusActive Status = iota // 0
StatusInactive // 1
StatusBanned // 2
)
// Statusから文字列へのマッピング
var statusNames = map[Status]string{
StatusActive: "active",
StatusInactive: "inactive",
StatusBanned: "banned",
}
// 文字列からStatusへのマッピング
var statusValues = map[string]Status{
"active": StatusActive,
"inactive": StatusInactive,
"banned": StatusBanned,
}
// MarshalJSON カスタムシリアライゼーション
func (s Status) MarshalJSON() ([]byte, error) {
name, ok := statusNames[s]
if !ok {
return json.Marshal("unknown")
}
return json.Marshal(name)
}
// UnmarshalJSON カスタムデシリアライゼーション
func (s *Status) UnmarshalJSON(data []byte) error {
var name string
if err := json.Unmarshal(data, &name); err != nil {
return err
}
val, ok := statusValues[name]
if !ok {
return fmt.Errorf("不明なステータス: %s", name)
}
*s = val
return nil
}
// EventLog イベントログエントリー
type EventLog struct {
Event string `json:"event"`
Timestamp CustomTime `json:"timestamp"`
Status Status `json:"status"`
Details string `json:"details,omitempty"`
}
func main() {
// === カスタムシリアライゼーションデモ ===
logEntry := EventLog{
Event: "user_register",
Timestamp: CustomTime{time.Date(2024, 1, 15, 14, 30, 0, 0, time.Local)},
Status: StatusActive,
Details: "新規ユーザーが正常に登録しました",
}
data, _ := json.MarshalIndent(logEntry, "", " ")
fmt.Println("=== カスタムシリアライゼーション ===")
fmt.Println(string(data))
// === カスタムデシリアライゼーションデモ ===
jsonStr := `{
"event": "user_login",
"timestamp": "2024/01/15",
"status": "inactive"
}`
var entry EventLog
err := json.Unmarshal([]byte(jsonStr), &entry)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\n解析結果: イベント=%s, 時刻=%s, ステータス=%d\n",
entry.Event,
entry.Timestamp.Format("2006-01-02 15:04:05"),
entry.Status,
)
// === ストリーミングデモ ===
fmt.Println("\n=== ストリーミングDecoder ===")
// ネットワークから受信したJSONストリームをシミュレーション
jsonStream := `[
{"name": "Alice", "score": 95},
{"name": "Bob", "score": 87},
{"name": "Charlie", "score": 92}
]`
decoder := json.NewDecoder(strings.NewReader(jsonStream))
// 開始トークンを読み取る
token, err := decoder.Token()
if err != nil {
log.Fatal(err)
}
fmt.Printf("開始トークン: %v\n", token)
// 配列の要素を1つずつ読み取る
type Student struct {
Name string `json:"name"`
Score int `json:"score"`
}
var students []Student
for decoder.More() {
var s Student
if err := decoder.Decode(&s); err != nil {
log.Fatal(err)
}
students = append(students, s)
}
for _, s := range students {
fmt.Printf("学生: %s, スコア: %d\n", s.Name, s.Score)
}
// === ストリーミングEncoderデモ ===
fmt.Println("\n=== ストリーミングEncoder ===")
var buf strings.Builder
encoder := json.NewEncoder(&buf)
encoder.SetIndent("", " ")
// 個々のオブジェクトをエンコード
for _, s := range students {
if err := encoder.Encode(s); err != nil {
log.Fatal(err)
}
}
fmt.Println(buf.String())
}
出力:
TEXT
=== カスタムシリアライゼーション ===
{
"event": "user_register",
"timestamp": "2024-01-15 14:30:00",
"status": "active",
"details": "新規ユーザーが正常に登録しました"
}
解析結果: イベント=user_login, 時刻=2024-01-15 00:00:00, ステータス=1
=== ストリーミングDecoder ===
開始トークン: [
学生: Alice, スコア: 95
学生: Bob, スコア: 87
学生: Charlie, スコア: 92
=== ストリーミングEncoder ===
{
"name": "Alice",
"score": 95
}
{
"name": "Bob",
"score": 87
}
{
"name": "Charlie",
"score": 92
}
応用シナリオ
シナリオ1:APIレスポンスラッパー
GO
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
// APIResponse 統一APIレスポンス構造
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// SuccessResponse 成功レスポンス
func SuccessResponse(w http.ResponseWriter, data interface{}) {
resp := APIResponse{
Code: 200,
Message: "success",
Data: data,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}
// ErrorResponse エラーレスポンス
func ErrorResponse(w http.ResponseWriter, statusCode int, errMsg string) {
resp := APIResponse{
Code: statusCode,
Message: "error",
Error: errMsg,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(resp)
}
// UserHandler ユーザーリクエストを処理
func UserHandler(w http.ResponseWriter, r *http.Request) {
// シミュレーション用ユーザーデータ
users := []map[string]interface{}{
{"id": 1, "name": "Alice", "role": "admin"},
{"id": 2, "name": "Bob", "role": "user"},
{"id": 3, "name": "Charlie", "role": "user"},
}
SuccessResponse(w, users)
}
func main() {
// シミュレーション用APIレスポンス
fmt.Println("=== シミュレーション用APIレスポンス ===")
// 成功レスポンス
successResp := APIResponse{
Code: 200,
Message: "success",
Data: map[string]interface{}{
"id": 1,
"name": "Alice",
},
}
data, _ := json.MarshalIndent(successResp, "", " ")
fmt.Println("成功レスポンス:")
fmt.Println(string(data))
// エラーレスポンス
errorResp := APIResponse{
Code: 404,
Message: "error",
Error: "ユーザーが見つかりません",
}
data, _ = json.MarshalIndent(errorResp, "", " ")
fmt.Println("\nエラーレスポンス:")
fmt.Println(string(data))
_ = log.Fatal // 未使用警告を回避
}
出力:
TEXT
=== シミュレーション用APIレスポンス ===
成功レスポンス:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"name": "Alice"
}
}
エラーレスポンス:
{
"code": 404,
"message": "error",
"error": "ユーザーが見つかりません"
}
シナリオ2:設定ファイルの読み取りとバリデーション
GO
package main
import (
"encoding/json"
"fmt"
"log"
"os"
)
// DatabaseConfig データベース設定
type DatabaseConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
DBName string `json:"dbname"`
}
// ServerConfig サーバー設定
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
ReadTimeout int `json:"read_timeout"`
WriteTimeout int `json:"write_timeout"`
AllowOrigins []string `json:"allow_origins"`
}
// AppConfig アプリケーション設定
type AppConfig struct {
AppName string `json:"app_name"`
Debug bool `json:"debug"`
Server ServerConfig `json:"server"`
Database DatabaseConfig `json:"database"`
}
// Validate 設定をバリデーション
func (c *AppConfig) Validate() error {
if c.AppName == "" {
return fmt.Errorf("app_nameは空にできません")
}
if c.Server.Port <= 0 || c.Server.Port > 65535 {
return fmt.Errorf("server.portは1〜65535の間でなければなりません")
}
if c.Database.Host == "" {
return fmt.Errorf("database.hostは空にできません")
}
return nil
}
func main() {
// シミュレーション用設定ファイル内容
configJSON := `{
"app_name": "GoWebApp",
"debug": true,
"server": {
"host": "0.0.0.0",
"port": 8080,
"read_timeout": 30,
"write_timeout": 30,
"allow_origins": ["http://localhost:3000", "https://example.com"]
},
"database": {
"host": "localhost",
"port": 3306,
"username": "root",
"password": "secret123",
"dbname": "myapp"
}
}`
// 設定を解析
var config AppConfig
err := json.Unmarshal([]byte(configJSON), &config)
if err != nil {
log.Fatalf("設定の解析に失敗しました: %v", err)
}
// 設定をバリデーション
if err := config.Validate(); err != nil {
log.Fatalf("設定バリデーション失敗: %v", err)
}
// 設定情報を出力
fmt.Printf("アプリ名: %s\n", config.AppName)
fmt.Printf("デバッグモード: %v\n", config.Debug)
fmt.Printf("サーバーアドレス: %s:%d\n", config.Server.Host, config.Server.Port)
fmt.Printf("データベース接続: %s:%d/%s\n",
config.Database.Host,
config.Database.Port,
config.Database.DBName,
)
fmt.Printf("許可オリジン: %v\n", config.Server.AllowOrigins)
// 書き込み例(変更された設定を保存)
config.Debug = false
config.Server.Port = 9090
output, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println("\n=== 変更された設定 ===")
fmt.Println(string(output))
// 実際のプロジェクトではファイルに書き込みます:
// os.WriteFile("config.json", output, 0644)
_ = os.WriteFile // 未使用警告を回避
}
出力:
TEXT
アプリ名: GoWebApp
デバッグモード: true
サーバーアドレス: 0.0.0.0:8080
データベース接続: localhost:3306/myapp
許可オリジン: [http://localhost:3000 https://example.com]
=== 変更された設定 ===
{
"app_name": "GoWebApp",
"debug": false,
"server": {
"host": "0.0.0.0",
"port": 9090,
"read_timeout": 30,
"write_timeout": 30,
"allow_origins": [
"http://localhost:3000",
"https://example.com"
]
},
"database": {
"host": "localhost",
"port": 3306,
"username": "root",
"password": "secret123",
"dbname": "myapp"
}
}
❓ よくある質問
質問1:なぜJSONフィールド名が大文字になるのですか?
理由: Goは先頭が大文字のフィールドのみをエクスポートし、json.Marshalはデフォルトでフィールド名をJSONキーとして使用します。
解決策: 構造体タグを使用して小文字の名前を指定します:
GO
type User struct {
Name string `json:"name"` // JSONでは "name"
Age int `json:"age"` // JSONでは "age"
Email string `json:"email"` // JSONでは "email"
}
質問2:空値のフィールドを無視するにはどうすればいいですか?
omitemptyタグを使用します:
GO
type Request struct {
Name string `json:"name"`
Email string `json:"email,omitempty"` // 空文字列の場合は省略
Age int `json:"age,omitempty"` // 0の場合は省略
Items []string `json:"items,omitempty"` // nilまたは空スライスの場合は省略
}
// テスト
req := Request{Name: "Alice"}
data, _ := json.Marshal(req)
fmt.Println(string(data))
// 出力: {"name":"Alice"} — email、age、itemsはすべて省略
質問3:JSONの数値精度問題をどう処理すればいいですか?
Goのjson.UnmarshalはデフォルトでJSONの数値をfloat64として解析するため、大きな整数では精度が失われます:
GO
// 問題の例
var result map[string]interface{}
json.Unmarshal([]byte(`{"id": 12345678901234567}`), &result)
fmt.Printf("%.0f\n", result["id"]) // 出力: 12345678901234568(精度が失われる!)
// 解決策:json.Numberを使用
decoder := json.NewDecoder(strings.NewReader(`{"id": 12345678901234567}`))
decoder.UseNumber()
decoder.Decode(&result)
id, _ := result["id"].(json.Number).Int64()
fmt.Println(id) // 出力: 12345678901234567(正しい)
質問4:構造が不明なJSONをどう処理すればいいですか?
map[string]interface{}またはjson.RawMessageを使用します:
GO
// 方法1:mapを使用
var data map[string]interface{}
json.Unmarshal(jsonBytes, &data)
// 方法2:遅延解析のためにjson.RawMessageを使用
type Message struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // 遅延解析
}
// Typeフィールドに基づいてPayloadの解析方法を決定
switch msg.Type {
case "user":
var user User
json.Unmarshal(msg.Payload, &user)
case "order":
var order Order
json.Unmarshal(msg.Payload, &order)
}
📖 まとめ
このレッスンではGoのJSON処理のコア内容をカバーしました:
- 基本操作:
json.Marshalでシリアライズ、json.Unmarshalでデシリアライズ - 構造体タグ:
json:"name"でフィールド名を制御、omitemptyで空値を省略、-でフィールドを無視 - ネスト処理:構造体のネスト、
map[string]interface{}で動的JSONを処理 - カスタムシリアライゼーション:
Marshaler/Unmarshalerインターフェースを実装 - ストリーミング:
json.Decoderとjson.Encoderでストリームデータを処理 - 実践応用:APIレスポンスラッパー、設定ファイル管理
💡 重要なポイント:
- 常にエラー処理をチェック
- デシリアライゼーションにはポインタを渡す
- 構造体タグを使用してJSONの命名規則を維持
- 大量データにはストリーミングを使用
📝 演習
演習1:基礎練習
Student構造体(名前、年齢、成績リストを持つ)を定義し、以下を実装するプログラムを書いてください:
- 3つの学生オブジェクトを作成
- JSON配列にシリアライズしてプリティプリント
- 構造体にデシリアライズして情報を出力
演習2:中級練習
シンプルなJSON設定マネージャーを実装してください:
- アプリケーション設定構造体を定義(サーバー、データベース、ログなどを含む)
LoadConfig(filename)関数でファイルから設定を読み込むSaveConfig(filename, config)関数で設定をファイルに保存- 設定バリデーションを実装
演習3:上級練習
JSON-RPCメッセージハンドラーを実装してください:
- リクエストとレスポンスの構造を定義
json.RawMessageで遅延解析を使用- リクエストメソッド名に基づいて異なるハンドラー関数にディスパッチ
- バッチリクエスト処理をサポート
GO
// ヒント:JSON-RPCリクエスト形式
type RPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
ID interface{} `json:"id"`
}
type RPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
ID interface{} `json:"id"`
}
次のレッスン
このレッスンを完了したら、レッスン22:HTTPサービスに進んでください。Goを使ってHTTPサーバーとクライアントを構築する方法を学びます。



