Go言語によるファイル入出力とJSON処理

ファイルI/OとJSONは、Goによるバックエンド開発において不可欠な2つのスキルです。os パッケージはシステムコールレベルのファイル操作を提供し、encoding/json パッケージはGoとJavaScriptの世界とのシームレスな通信を可能にします。

Go標準ライブラリにおけるファイル操作とJSON処理は、巧妙かつ統一されたアプローチで設計されています。これらはすべて、io.Readerおよびio.Writerインターフェースに基づいています。このレッスンでは、ファイルI/OとJSON処理に必要なすべての基本スキルを習得します。

1. 学習内容


2. フルスタックエンジニアの実話

(1) 課題:JSON文字列の手動連結

チャーリーはフルスタックエンジニアで、フロントエンドで使用するために、データベースからユーザーデータをJSONファイルにエクスポートする必要があります:

「サードパーティのライブラリを使いたくなかったので、JSON文字列を手作業で作成しました。ところが、ユーザー名フィールドに"という文字が含まれており、これがJSONのフォーマットを完全に崩してしまいました。エラーが表示されたのは、100万行のデータのエクスポートが半分ほど進んだ段階になってからで、変更をロールバックするのにさらに2時間かかってしまいました。」

彼は自分が書いたコードを開いた:

GO
// 不良コード:手動でのつなぎ合わせ JSON 文字列
func exportUserJSON(users []User) string {
    result := "["
    for i, u := range users {
        if i > 0 {
            result += ","
        }
        // 手動でのつなぎ合わせ,ダブルクォートがエスケープされていない
        result += "{\"name\":\"" + u.Name + "\",\"age\":" + string(u.Age) + "}"
    }
    result += "]"
    return result
}

u.Name" または \ が含まれている場合、生成される JSON は破損します。さらに、string(u.Age) は数値を ASCII に変換するため、28\x1c になります。

(2) Go ソリューション:encoding/json + ファイル I/O

GO
// json_exporter.go
パッケージ main

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

type User struct {
    Name 文字列 `json:"name"`
    Age  int    `json:"age"`
    City 文字列 `json:"city"`
}

func main() {
    users := []User{
        {Name: "Alice", Age: 28, City: "Shanghai"},
        {Name: `Bob "The Builder"`, Age: 32, City: "Beijing"},
        {Name: "Charlie", Age: 25, City: "Guangzhou"},
    }

    // JSON ファイルへのシリアライズ(手動でのつなぎ合わせは不要)
    file, _ := os.Create("users.json")
    defer file.Close()

    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ")
    encoder.Encode(users)

    fmt.Println("エクスポートに成功しました!")

    // 検証:後で読み返す
    data, _ := os.ReadFile("users.json")
    fmt.Println(文字列(data))
}

出力:

TEXT
エクスポートに成功しました!
[
  {
    "name": "Alice",
    "age": 28,
    "city": "Shanghai"
  },
  {
    "name": "Bob \"The Builder\"",
    "age": 32,
    "city": "Beijing"
  },
  {
    "name": "Charlie",
    "age": 25,
    "city": "Guangzhou"
  }
]

(3) メリット:JSON処理の比較

方法 ダブルクォートのエスケープ 特殊文字 大容量ファイル コード量
手動での連結 ❌ 手動でのエスケープ ❌ エラーが発生しやすい ❌ OOM 約50行
json.Marshal ✅ 自動 ✅ 自動 ❌ メモリ満杯 約5行
json.Encoder ✅ 自動 ✅ 自動 ✅ ストリームベース 約5行
💡 ヒント: JSONを手動で作成してはいけませんencoding/json パッケージを使用してください。このパッケージは、エスケープ、エンコーディング、インデントを自動的に処理し、GoのUTF-8文字も正しく扱います。


3. OSのファイル操作

(1) ファイルの開く・作成

GO
package main

import (
    "fmt"
    "os"
)

func main() {
    // 作成(または切り取り)ファイル
    f, _ := os.Create("test.txt")
    defer f.Close()

    // 文字列の書き込み
    f.WriteString("Hello, Go Files!\n")
    f.Write([]byte("Second line\n"))

    // 追加モードを有効にする
    f2, _ := os.OpenFile("test.txt", os.O_APPEND|os.O_WRONLY, 0644)
    defer f2.Close()
    f2.WriteString("Appended line\n")

    // ファイルを読み込む
    data, _ := os.ReadFile("test.txt")
    fmt.Print(string(data))
}

出力:

TEXT
Hello, Go Files!
Second line
Appended line

(2) os.ReadFile / os.WriteFile(1回限りの読み取り/書き込み)

GO
パッケージ main

import (
    "fmt"
    "os"
)

func main() {
    // 書き込み(使い捨て)
    content := []byte("line1\nline2\nline3\n")
    err := os.WriteFile("data.txt", content, 0644)
    if err != nil {
        fmt.Printf("Write エラー: %v\n", err)
        return
    }

    // 読み込み(使い捨て)
    data, err := os.ReadFile("data.txt")
    if err != nil {
        fmt.Printf("Read エラー: %v\n", err)
        return
    }
    fmt.Printf("Read %d bytes:\n%s", len(data), data)
}

▶ サンプル:ファイルのコピー

GO
package main

import (
    "fmt"
    "io"
    "os"
)

func copyFile(src, dst string) (int64, error) {
    sourceFile, err := os.Open(src)
    if err != nil {
        return 0, err
    }
    defer sourceFile.Close()

    destFile, err := os.Create(dst)
    if err != nil {
        return 0, err
    }
    defer destFile.Close()

    // io.Copy デフォルトを使用する 32KB バッファ
    return io.Copy(destFile, sourceFile)
}

func main() {
    // まずソースファイルを作成します
    os.WriteFile("source.txt", []byte("Hello World!\n"), 0644)

    n, err := copyFile("source.txt", "dest.txt")
    if err != nil {
        fmt.Printf("Copy error: %v\n", err)
        return
    }
    fmt.Printf("Copied %d bytes\n", n)

    data, _ := os.ReadFile("dest.txt")
    fmt.Printf("Content: %s", data)
}
▶ 試してみよう

出力:

TEXT
Copied 13 bytes
Content: Hello World!
🔥 よくある間違い: os.ReadFile はファイル全体をメモリに読み込みます。これは小さなファイル(100MB未満)には適していますが、大きなファイル(1GB以上のログなど)を処理する場合は、ストリーミング処理を行うために bufio または io.Copy を使用する必要があります。


4. bufio バッファ付きI/O

(1) bufioは行単位で読み込む

GO
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    // まずテストファイルを作成する
    content := ""
    for i := 1; i <= 100; i++ {
        content += fmt.Sprintf("Line %d\n", i)
    }
    os.WriteFile("large.txt", []byte(content), 0644)

    // 行単位で読み込む
    file, _ := os.Open("large.txt")
    defer file.Close()

    scanner := bufio.NewScanner(file)
    lineCount := 0
    for scanner.Scan() {
        lineCount++
        if lineCount <= 3 {
            fmt.Println(scanner.Text())
        }
    }
    fmt.Printf("...計 %d 了解\n", lineCount)
}

出力:

TEXT
Line 1
Line 2
Line 3
...計 100 了解

▶ サンプル:bufio バッファ書き込み

GO
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, _ := os.Create("buffered.txt")
    defer file.Close()

    writer := bufio.NewWriter(file)
    writer.WriteString("1行目\n")
    writer.WriteString("2行目\n")
    writer.WriteString("3行目\n")

    fmt.Printf("バッファサイズ: %d\n", writer.Buffered())

    // 重要:バッファをディスクに書き込み
    writer.Flush()

    data, _ := os.ReadFile("buffered.txt")
    fmt.Printf("内容:\n%s", data)
}
▶ 試してみよう

(2) os 対 bufio

機能 OSへの直接読み書き Bufioバッファリング
内部メカニズム システムコール(読み取り/書き込み操作ごとに1回のシステムコール) ユーザーモードバッファ(システムコールの回数を削減)
適したシナリオ 小さなファイル、ランダムアクセス 大きなファイル、順次読み書き
行単位で読み込む 未対応 bufio.Scanner
パフォーマンス(大容量ファイル) 遅い(システムコールの回数が多い) 速い(システムコールが90%削減)
デフォルトのバッファ 該当なし 4 KB(設定可能)

5. encoding/json によるシリアライズ

(1) json.Marshal / json.Unmarshal

GO
package main

import (
    "encoding/json"
    "fmt"
)

type Product struct {
    Name     string   `json:"name"`
    Price    float64  `json:"price"`
    InStock  bool     `json:"in_stock"`
    Tags     []string `json:"tags,omitempty"`
    Internal string   `json:"-"`  // シリアライズしない
}

func main() {
    p := Product{
        Name:     "Go Mug",
        Price:    19.99,
        InStock:  true,
        Tags:     []string{"gift", "office"},
        Internal: "secret123",
    }

    // シリアライズ
    data, _ := json.Marshal(p)
    fmt.Printf("Marshaled: %s\n", data)

    // インデント付き
    pretty, _ := json.MarshalIndent(p, "", "  ")
    fmt.Printf("Pretty:\n%s\n", pretty)

    // デシリアライズ
    var p2 Product
    json.Unmarshal([]byte(`{"name":"Go T-Shirt","price":29.99,"in_stock":false}`), &p2)
    fmt.Printf("Unmarshaled: %+v\n", p2)
}

出力:

TEXT
Marshaled: {"name":"Go Mug","price":19.99,"in_stock":true,"tags":["gift","office"]}
Pretty:
{
  "name": "Go Mug",
  "price": 19.99,
  "in_stock": true,
  "tags": [
    "gift",
    "office"
  ]
}
Unmarshaled: {Name:Go T-Shirt Price:29.99 InStock:false Tags:[] Internal:}

▶ サンプル:動的なJSONの解析 (interface{})

GO
パッケージ main

import (
    "encoding/json"
    "fmt"
)

func main() {
    data := `{"name":"Alice","age":28,"address":{"city":"Shanghai","zip":"200000"}}`

    // 解析の結果、 map(事前定義は不要です struct)
    var result map[文字列]interface{}
    json.Unmarshal([]byte(data), &result)

    fmt.Printf("name: %v\n", result["name"])
    fmt.Printf("age: %v (type=%T)\n", result["age"], result["age"])

    // ネスト map 型アサーションが必要
    addr := result["address"].(map[文字列]interface{})
    fmt.Printf("city: %v\n", addr["city"])
}
▶ 試してみよう

出力:

TEXT
name: Alice
age: 28 (type=float64)
city: Shanghai
💡 ヒント: JSON内の数値は、デフォルトで float64 として解析されます。つまり、age は 28 として表示されますが、その型は float64 となります。int を使用したい場合は、構造体(struct)を使用して型を指定するか、json.DecoderUseNumber() を組み合わせて使用してください。


6. JSONエンコーダー/デコーダーのストリーミング処理

▶ サンプル:エンコーダーがファイルにリアルタイムで書き込む

GO
package main

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

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    users := []User{
        {Name: "Alice", Age: 28},
        {Name: "Bob", Age: 32},
        {Name: "Charlie", Age: 25},
    }

    // ファイルへのストリーム書き込み(完全には構成されていない []byte)
    file, _ := os.Create("users.json")
    defer file.Close()

    encoder := json.NewEncoder(file)
    encoder.SetIndent("", "  ")

    for _, u := range users {
        encoder.Encode(u)  // 項目ごとに記入する
    }

    data, _ := os.ReadFile("users.json")
    fmt.Println(string(data))
}
▶ 試してみよう

(2) デコーダ:ストリームベースの読み込み(行単位のJSON)

GO
パッケージ main

import (
    "encoding/json"
    "fmt"
    "os"
    "strings"
)

type User struct {
    Name 文字列 `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    // シミュレーション JSON Lines 書式(1行に1つずつ JSON オブジェクト)
    input := `{"name":"Alice","age":28}
{"name":"Bob","age":32}
{"name":"Charlie","age":25}`

    // ストリームデコード
    decoder := json.NewDecoder(strings.NewReader(input))
    for {
        var u User
        err := decoder.Decode(&u)
        if err != nil {
            break  // EOF
        }
        fmt.Printf("Decoded: %s (%d)\n", u.Name, u.Age)
    }
}

出力:

TEXT
Decoded: Alice (28)
Decoded: Bob (32)
Decoded: Charlie (25)

(3) マーシャル対エンコーダー

プロパティ json.Marshal json.Encoder
出力先 []byte (メモリ) io.Writer (任意の出力先)
メモリ使用量 ビルド完了 []バイト ストリーム書き込みバッファ
大容量ファイル ❌ OOMのリスク ✅ セキュリティ
インデント制御 MarshalIndent SetIndent()
代表的なシナリオ APIの応答/小規模データ ファイル書き込み/ネットワークストリーム

7. JSONタグの完全ガイド

(1) タグオプション

GO
type Config struct {
    Name     string   `json:"name"`               // フィールド名のマッピング
    Omit     string   `json:"omit,omitempty"`      // ゼロのときは無視する
    Skip     string   `json:"-"`                   // このフィールドをスキップする
    String   int      `json:"string"`              // 数値を文字列に変換する
}

(2) すべてのオプション

オプション 構文 効果
名前変更 json:"new_name" JSONフィールド名を「new_name」に変更
omitempty json:"name,omitempty" 値がゼロの場合はこのフィールドを省略
スキップ json:"-" シリアライズ/デシリアライズしない
文字列 json:"id,string" 数値を文字列に変換する(JavaScript互換)
ネスト json:"-" 中間構造体 ネストされたシリアライゼーションの制御

▶ サンプル:omitempty + 文字列の実用例

GO
パッケージ main

import (
    "encoding/json"
    "fmt"
)

type APIResponse struct {
    Code    int    `json:"code"`
    Message 文字列 `json:"message,omitempty"`  // 空欄の場合は省略
    Data    any    `json:"data,omitempty"`      // nil 時刻は省略
    ID      int64  `json:"id,文字列"`           // int64 → 文字列
}

func main() {
    resp := APIResponse{
        Code: 200,
        Data: nil,     // 省略される
        ID:   9876543210123,
    }

    data, _ := json.MarshalIndent(resp, "", "  ")
    fmt.Println(文字列(data))
}
▶ 試してみよう

出力:

TEXT
{
  "code": 200,
  "id": "9876543210123"
}

8. 完全な例:データベースのJSONエクスポートツール

GO
// db_exporter.go
package main

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

// ---------- データモデル ----------

type Product struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Price     float64   `json:"price"`
    Stock     int       `json:"stock"`
    Category  string    `json:"category,omitempty"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at,omitempty"`
}

type ExportConfig struct {
    FilePath     string `json:"file_path"`
    PrettyPrint  bool   `json:"pretty_print"`
    IncludeMeta  bool   `json:"include_meta"`
}

// ---------- シミュレーションデータベース ----------

type Database struct {
    products []Product
}

func NewDatabase() *Database {
    return &Database{
        products: []Product{
            {ID: 1, Name: "Go Mug", Price: 19.99, Stock: 100, Category: "accessories",
                CreatedAt: time.Now()},
            {ID: 2, Name: `Go "Gopher" T-Shirt`, Price: 29.99, Stock: 50, Category: "clothing",
                CreatedAt: time.Now()},
            {ID: 3, Name: "Go Programming Book", Price: 49.99, Stock: 200, Category: "books",
                CreatedAt: time.Now()},
        },
    }
}

func (db *Database) QueryAll() []Product {
    return db.products
}

// ---------- エクスポートツール ----------

type Exporter struct {
    config ExportConfig
}

func NewExporter(config ExportConfig) *Exporter {
    return &Exporter{config: config}
}

func (e *Exporter) ExportToFile(products []Product) error {
    file, err := os.Create(e.config.FilePath)
    if err != nil {
        return fmt.Errorf("create file: %w", err)
    }
    defer file.Close()

    encoder := json.NewEncoder(file)
    if e.config.PrettyPrint {
        encoder.SetIndent("", "  ")
    }

    if e.config.IncludeMeta {
        wrapper := map[string]interface{}{
            "exported_at": time.Now().Format(time.RFC3339),
            "total":       len(products),
            "products":    products,
        }
        return encoder.Encode(wrapper)
    }

    return encoder.Encode(products)
}

func (e *Exporter) ExportToMemory(products []Product) ([]byte, error) {
    if e.config.PrettyPrint {
        return json.MarshalIndent(products, "", "  ")
    }
    return json.Marshal(products)
}

// ---------- インポーター(読み込み JSON ファイル)----------

type Importer struct{}

func NewImporter() *Importer {
    return &Importer{}
}

func (imp *Importer) ImportFromFile(path string) ([]Product, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("open file: %w", err)
    }
    defer file.Close()

    var products []Product
    decoder := json.NewDecoder(file)
    if err := decoder.Decode(&products); err != nil {
        // 試着してみる wrapper の形式
        file.Seek(0, 0)
        var wrapped struct {
            Products []Product `json:"products"`
        }
        if err2 := decoder.Decode(&wrapped); err2 != nil {
            return nil, fmt.Errorf("decode: %w", err)
        }
        products = wrapped.Products
    }

    return products, nil
}

func main() {
    db := NewDatabase()
    products := db.QueryAll()

    // 1. フロー JSON ファイルへの書き込み
    exporter := NewExporter(ExportConfig{
        FilePath:    "export.json",
        PrettyPrint: true,
        IncludeMeta: true,
    })
    if err := exporter.ExportToFile(products); err != nil {
        fmt.Printf("Export error: %v\n", err)
        return
    }
    fmt.Println("✅ エクスポート先 export.json")

    // 2. 読み込んで検証する
    importer := NewImporter()
    imported, err := importer.ImportFromFile("export.json")
    if err != nil {
        fmt.Printf("Import error: %v\n", err)
        return
    }
    fmt.Printf("✅ インポート %d 製品\n", len(imported))
    for _, p := range imported {
        fmt.Printf("  %d. %s ($%.2f)\n", p.ID, p.Name, p.Price)
    }

    // 3. ファイルの内容を表示する
    data, _ := os.ReadFile("export.json")
    fmt.Printf("\nファイルの内容:\n%s\n", data)
}

期待される出力:

TEXT
✅ エクスポート先 export.json
✅ インポート 3 製品
  1. Go Mug ($19.99)
  2. Go "Gopher" T-Shirt ($29.99)
  3. Go Programming Book ($49.99)

ファイルの内容:
{
  "exported_at": "2026-07-08T10:00:00Z",
  "total": 3,
  "products": [
    {
      "id": 1,
      "name": "Go Mug",
      "price": 19.99,
      "stock": 100,
      "category": "accessories",
      "created_at": "2026-07-08T10:00:00Z"
    },
    ...
  ]
}
100%
flowchart LR
    A[Go struct] --> B[json.Marshal]
    A --> C[json.Encoder]
    B --> D[[]byte メモリ]
    D --> E[os.WriteFile / HTTP Response]
    C --> F[File / net.Conn / bytes.Buffer]
    F --> G[ストリーム書き込み]
    H[JSON ファイル/流れ] --> I[json.Decoder]
    I --> J[Go struct]
    H --> K[json.Unmarshal]
    K --> J
    style A fill:#e1f5fe
    style J fill:#e1f5fe
🔥 よくある間違い: JSON エクスポート時の omitempty オプションは、値が 0 の時刻 (time.Time{}) には機能しません。—time.Time{}nil とは異なり、省略されることはありません。値が 0 の時刻を省略する必要がある場合は、ポインタ型 *time.Time を使用してください。


❓ よくある質問

Q os.ReadFileioutil.ReadFile の違いは何ですか?
A os.ReadFile は Go 1.16 で導入された代替機能であり、動作は同じです。ioutil.ReadFileはGo 1.19以降で非推奨となりました。os.ReadFile / os.WriteFileを使用してください。
Q bufioは、OSを使った直接の読み書きに比べて、なぜ高速なのですか?
A bufioはユーザー空間にバッファ(デフォルトで4KB)を維持しており、これによりシステムコールの回数が大幅に削減されます。直接の os.File.Read 呼び出しは常にシステムコールを発生させ(約 1 µs のオーバーヘッドを伴う)、一方、bufio は一度に 4 KB を読み込むため、システムコールを 99.9% 削減します。
Q json.Marshaljson.Encoder のどちらを選べばよいですか?
A データサイズが 100KB 未満の場合、またはインメモリ処理が必要な場合は → Marshal;ファイルやネットワークストリームへの書き込み、あるいは大容量ファイルの場合は → EncoderEncoderの利点は、ストリーミング方式で動作し、メモリをあまり消費しないことです。
Q struct tag は JSON フィールドをどのように制御しますか?
A json:"name" はフィールド名を変更します。json:"name,omitempty" は 0 の値を省略します。json:"-" はフィールドをスキップします。json:"id,string" は数値を文字列に変換します。複数のオプションを指定する場合は、カンマで区切ってください。
Q JSON では中国語のエンコーディングはどのように扱われますか?
A Go の encoding/json はデフォルトで UTF-8 を出力します。非 ASCII 文字はそのまま出力されます。ASCII 文字のみの出力が必要な場合(一部のレガシーシステムで要求される場合など)、json.MarshalIndent または SetEscapeHTML(false) を使用してください。
Q 大きなJSONファイルはどのように処理すればよいですか?
A ストリーミングデコードには json.Decoder を使用してください(decoder.Decode(&v) は1回に1つのオブジェクトをデコードします)。あるいは、JSON Lines形式(1行に1つのJSONオブジェクト)を使用してください。ファイル全体を読み込むために json.Unmarshal を使用しないでください。
Q 構造が不明なJSONをどのように解析すればよいですか?
A map[string]interface{} または json.RawMessage(遅延解析)を使用してください。map[string]interface{} は柔軟性がありますが、型アサーションが必要です。json.RawMessage は、後で解析できるよう生のJSONを保持します。
Q os.Createos.OpenFile の違いは何ですか?
A os.Create(f)os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) と同等です。つまり、ファイルがすでに存在する場合、そのファイルを切り捨て(上書き)します。os.OpenFileは、より多くのモード(追記、読み取り専用、新規ファイルの作成など)に対応しています。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐)os.ReadFile を使用してテキストファイルを読み込み、行数、単語数、文字数をカウントし、その結果を別のファイルに書き出すプログラムを作成してください。strings.Fieldsbufio.Scanner、および os.WriteFile を使用する必要があります。

  2. 上級問題(難易度 ⭐⭐):項目の追加、一覧表示、完了、削除をサポートし、TodoList を使用してデータをファイルにシリアライズして永続化する TodoList マネージャーを実装してください。要件:JSON タグを使用してフィールド名を制御し、omitempty を使用してオプションのフィールドを処理してください。

  3. 課題(難易度 ⭐⭐⭐)[]Product データソースから、JSON、JSON Lines(1行につき1つのJSONオブジェクト)、CSVの3つの形式で同時にデータをエクスポートするマルチフォーマットエクスポーターを実装してください。os.Createjson.Encoder(JSON用)/fmt.Fprintf(CSV用)および共有の io.Writer インターフェースを使用する必要があります。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%