Go言語によるファイル入出力とJSON処理
ファイルI/OとJSONは、Goによるバックエンド開発において不可欠な2つのスキルです。
osパッケージはシステムコールレベルのファイル操作を提供し、encoding/jsonパッケージはGoとJavaScriptの世界とのシームレスな通信を可能にします。
Go標準ライブラリにおけるファイル操作とJSON処理は、巧妙かつ統一されたアプローチで設計されています。これらはすべて、io.Readerおよびio.Writerインターフェースに基づいています。このレッスンでは、ファイルI/OとJSON処理に必要なすべての基本スキルを習得します。
1. 学習内容
- OSのファイル操作(作成/開く/読み取り/書き込み)
os.ReadFile/os.WriteFile: 1回の操作で読み書きを行う- bufio バッファ付き読み書き(行単位/大容量ファイル)
encoding/jsonマーシャル / アンマーシャル- JSONエンコーダー/デコーダーのストリーミング処理
- JSONタグはシリアライゼーションの挙動を制御します
- ファイル入出力とJSONを使用して、データベースのエクスポートツールを構築する
2. フルスタックエンジニアの実話
(1) 課題:JSON文字列の手動連結
チャーリーはフルスタックエンジニアで、フロントエンドで使用するために、データベースからユーザーデータをJSONファイルにエクスポートする必要があります:
「サードパーティのライブラリを使いたくなかったので、JSON文字列を手作業で作成しました。ところが、ユーザー名フィールドに
"という文字が含まれており、これがJSONのフォーマットを完全に崩してしまいました。エラーが表示されたのは、100万行のデータのエクスポートが半分ほど進んだ段階になってからで、変更をロールバックするのにさらに2時間かかってしまいました。」
彼は自分が書いたコードを開いた:
// 不良コード:手動でのつなぎ合わせ 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
// 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))
}
出力:
エクスポートに成功しました!
[
{
"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行 |
encoding/json パッケージを使用してください。このパッケージは、エスケープ、エンコーディング、インデントを自動的に処理し、GoのUTF-8文字も正しく扱います。
3. OSのファイル操作
(1) ファイルの開く・作成
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))
}
出力:
Hello, Go Files!
Second line
Appended line
(2) os.ReadFile / os.WriteFile(1回限りの読み取り/書き込み)
パッケージ 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)
}
▶ サンプル:ファイルのコピー
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)
}
出力:
Copied 13 bytes
Content: Hello World!
os.ReadFile はファイル全体をメモリに読み込みます。これは小さなファイル(100MB未満)には適していますが、大きなファイル(1GB以上のログなど)を処理する場合は、ストリーミング処理を行うために bufio または io.Copy を使用する必要があります。
4. bufio バッファ付きI/O
(1) bufioは行単位で読み込む
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)
}
出力:
Line 1
Line 2
Line 3
...計 100 了解
▶ サンプル:bufio バッファ書き込み
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
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)
}
出力:
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{})
パッケージ 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"])
}
出力:
name: Alice
age: 28 (type=float64)
city: Shanghai
float64 として解析されます。つまり、age は 28 として表示されますが、その型は float64 となります。int を使用したい場合は、構造体(struct)を使用して型を指定するか、json.Decoder と UseNumber() を組み合わせて使用してください。
6. JSONエンコーダー/デコーダーのストリーミング処理
▶ サンプル:エンコーダーがファイルにリアルタイムで書き込む
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)
パッケージ 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)
}
}
出力:
Decoded: Alice (28)
Decoded: Bob (32)
Decoded: Charlie (25)
(3) マーシャル対エンコーダー
| プロパティ | json.Marshal | json.Encoder |
|---|---|---|
| 出力先 | []byte (メモリ) |
io.Writer (任意の出力先) |
| メモリ使用量 | ビルド完了 []バイト | ストリーム書き込みバッファ |
| 大容量ファイル | ❌ OOMのリスク | ✅ セキュリティ |
| インデント制御 | MarshalIndent |
SetIndent() |
| 代表的なシナリオ | APIの応答/小規模データ | ファイル書き込み/ネットワークストリーム |
7. JSONタグの完全ガイド
(1) タグオプション
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 + 文字列の実用例
パッケージ 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))
}
出力:
{
"code": 200,
"id": "9876543210123"
}
8. 完全な例:データベースのJSONエクスポートツール
// 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)
}
期待される出力:
✅ エクスポート先 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"
},
...
]
}
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
omitempty オプションは、値が 0 の時刻 (time.Time{}) には機能しません。—time.Time{} は nil とは異なり、省略されることはありません。値が 0 の時刻を省略する必要がある場合は、ポインタ型 *time.Time を使用してください。
❓ よくある質問
os.ReadFile と ioutil.ReadFile の違いは何ですか?os.ReadFile は Go 1.16 で導入された代替機能であり、動作は同じです。ioutil.ReadFileはGo 1.19以降で非推奨となりました。os.ReadFile / os.WriteFileを使用してください。os.File.Read 呼び出しは常にシステムコールを発生させ(約 1 µs のオーバーヘッドを伴う)、一方、bufio は一度に 4 KB を読み込むため、システムコールを 99.9% 削減します。json.Marshal と json.Encoder のどちらを選べばよいですか?Marshal;ファイルやネットワークストリームへの書き込み、あるいは大容量ファイルの場合は → Encoder。Encoderの利点は、ストリーミング方式で動作し、メモリをあまり消費しないことです。struct tag は JSON フィールドをどのように制御しますか?json:"name" はフィールド名を変更します。json:"name,omitempty" は 0 の値を省略します。json:"-" はフィールドをスキップします。json:"id,string" は数値を文字列に変換します。複数のオプションを指定する場合は、カンマで区切ってください。encoding/json はデフォルトで UTF-8 を出力します。非 ASCII 文字はそのまま出力されます。ASCII 文字のみの出力が必要な場合(一部のレガシーシステムで要求される場合など)、json.MarshalIndent または SetEscapeHTML(false) を使用してください。json.Decoder を使用してください(decoder.Decode(&v) は1回に1つのオブジェクトをデコードします)。あるいは、JSON Lines形式(1行に1つのJSONオブジェクト)を使用してください。ファイル全体を読み込むために json.Unmarshal を使用しないでください。map[string]interface{} または json.RawMessage(遅延解析)を使用してください。map[string]interface{} は柔軟性がありますが、型アサーションが必要です。json.RawMessage は、後で解析できるよう生のJSONを保持します。os.Create と os.OpenFile の違いは何ですか?os.Create(f) は os.OpenFile(f, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) と同等です。つまり、ファイルがすでに存在する場合、そのファイルを切り捨て(上書き)します。os.OpenFileは、より多くのモード(追記、読み取り専用、新規ファイルの作成など)に対応しています。📖 まとめ
os.ReadFile/os.WriteFile: 1回の操作で小さなファイルの読み書きを行うbufio.Scannerは大きなファイルを行単位で処理し、bufio.Writerは書き込みをバッファリングしますencoding/jsonパッケージはシリアライズとデシリアライズを自動的に処理するため、手動で文字列を構築する必要がなくなりますjson.Marshalはメモリ内のデータを処理し、json.Encoderはデータをストリームに書き込みますjson.Decoderはデータをストリームとして読み込むため、大容量のファイルに適しています- JSONタグ:
omitempty/-/stringはシリアライゼーションの挙動を制御します - ファイルI/OとJSONは、
io.Reader/io.Writerインターフェースを通じて統合されています
📝 練習問題
-
基本問題(難易度 ⭐):
os.ReadFileを使用してテキストファイルを読み込み、行数、単語数、文字数をカウントし、その結果を別のファイルに書き出すプログラムを作成してください。strings.Fields、bufio.Scanner、およびos.WriteFileを使用する必要があります。 -
上級問題(難易度 ⭐⭐):項目の追加、一覧表示、完了、削除をサポートし、
TodoListを使用してデータをファイルにシリアライズして永続化するTodoListマネージャーを実装してください。要件:JSON タグを使用してフィールド名を制御し、omitemptyを使用してオプションのフィールドを処理してください。 -
課題(難易度 ⭐⭐⭐):
[]Productデータソースから、JSON、JSON Lines(1行につき1つのJSONオブジェクト)、CSVの3つの形式で同時にデータをエクスポートするマルチフォーマットエクスポーターを実装してください。os.Create+json.Encoder(JSON用)/fmt.Fprintf(CSV用)および共有のio.Writerインターフェースを使用する必要があります。



