Go言語でのファイル入出力とJSONシリアライズの実践ガイド

ファイルI/Oとストリームの基本概念

アプリケーション開発において、ファイル操作はデータの永続化や外部連携に不可欠な要素です。Go言語では、ファイルデータを「ストリーム(連続したデータの流れ)」として抽象化して扱います。データがファイル(データソース)からプログラムのメモリへと読み込まれる経路を「入力ストリーム(Reader)」、逆にメモリからファイルへと書き出される経路を「出力ストリーム(Writer)」と呼びます。

ファイルの読み込み操作

ファイルを読み込むアプローチは大きく分けて2つあります。メモリ消費を抑えるためのバッファリングを用いた逐次読み込みと、小規模なファイル向けのメモリへの一括読み込みです。

1. バッファリングを用いた逐次読み込み

ログファイルや大きなCSVファイルなどを扱う際は、bufio パッケージを利用して一行ずつ処理するのが効率的です。

package main

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

func main() {
	filePath := "config/app_settings.yaml"
	file, err := os.Open(filePath)
	if err != nil {
		log.Fatalf("ファイルオープンエラー: %v", err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	lineNum := 1
	for scanner.Scan() {
		fmt.Printf("[%d] %s\n", lineNum, scanner.Text())
		lineNum++
	}

	if err := scanner.Err(); err != nil {
		log.Printf("読み込み中にエラーが発生しました: %v", err)
	}
}

2. ファイル全体の一括読み込み

設定ファイルなど、サイズが小さくメモリに展開しても問題ないファイルは os.ReadFile を使用するとコードが簡潔になります。

package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	data, err := os.ReadFile("data/metadata.txt")
	if err != nil {
		log.Fatalf("読み込み失敗: %v", err)
	}
	fmt.Printf("ファイル内容:\n%s\n", string(data))
}

ファイルへの書き込みとモード指定

ファイルへの書き込みには os.OpenFile を使用します。この際、ファイルのオープンモード(フラグ)とパーミッションを適切に指定する必要があります。

主なフラグには以下があります:

  • os.O_CREATE: ファイルが存在しない場合に新規作成
  • os.O_WRONLY: 書き込み専用でオープン
  • os.O_APPEND: ファイルの末尾に追記
  • os.O_TRUNC: 既存のファイルを切り詰め(上書き初期化)

バッファリングを用いた書き込みと追記

書き込みパフォーマンスを向上させるため、bufio.Writer を介してバッファにデータを蓄積し、最後にディスクへフラッシュします。

package main

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

func main() {
	// 追記モードでファイルを開く
	file, err := os.OpenFile("logs/server.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatalf("ファイル作成エラー: %v", err)
	}
	defer file.Close()

	writer := bufio.NewWriter(file)
	messages := []string{
		"INFO: Server started on port 8080",
		"WARN: High memory usage detected",
		"INFO: Request processed successfully",
	}

	for _, msg := range messages {
		if _, err := writer.WriteString(msg + "\n"); err != nil {
			log.Printf("書き込みエラー: %v", err)
		}
	}

	// バッファの内容をディスクにフラッシュ
	if err := writer.Flush(); err != nil {
		log.Fatalf("フラッシュエラー: %v", err)
	}
	fmt.Println("ログの書き込みが完了しました。")
}

ファイルのユーティリティ操作

ファイルの存在確認

Goではファイルの存在確認に os.Stat を利用し、返されるエラーを os.IsNotExist で判定します。

package main

import (
	"fmt"
	"os"
)

func isFileExist(path string) bool {
	_, err := os.Stat(path)
	if err == nil {
		return true
	}
	return !os.IsNotExist(err) // 権限エラーなどの場合はtrueを返す
}

func main() {
	path := "data/report.csv"
	if isFileExist(path) {
		fmt.Printf("%s は存在します。\n", path)
	} else {
		fmt.Printf("%s は見つかりません。\n", path)
	}
}

ファイルのコピー

io.Copy を使用すると、Reader から Writer へ効率的にデータをストリーミングコピーできます。大きなファイルでもメモリを圧迫しません。

package main

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

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

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

	bytesWritten, err := io.Copy(destFile, sourceFile)
	if err != nil {
		return err
	}
	fmt.Printf("%d バイトコピーしました。\n", bytesWritten)
	return nil
}

func main() {
	if err := copyFile("assets/image.png", "backups/image_backup.png"); err != nil {
		log.Fatalf("コピー失敗: %v", err)
	}
}

テキスト解析と文字カウント

ファイル内の文字種別をカウントするロジックの例です。Unicodeプロパティを利用して分類します。

package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"unicode"
)

type TextStats struct {
	UpperCount int
	LowerCount int
	DigitCount int
	PunctCount int
}

func main() {
	file, err := os.Open("docs/article.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	var stats TextStats
	scanner := bufio.NewScanner(file)
	
	for scanner.Scan() {
		for _, char := range scanner.Text() {
			switch {
			case unicode.IsUpper(char):
				stats.UpperCount++
			case unicode.IsLower(char):
				stats.LowerCount++
			case unicode.IsDigit(char):
				stats.DigitCount++
			case unicode.IsPunct(char):
				stats.PunctCount++
			}
		}
	}

	fmt.Printf("大文字: %d, 小文字: %d, 数字: %d, 記号: %d\n",
		stats.UpperCount, stats.LowerCount, stats.DigitCount, stats.PunctCount)
}

コマンドライン引数の処理

アプリケーションの設定をコマンドラインから受け取る場合、標準ライブラリの flag パッケージが非常に有用です。型の自動変換やヘルプメッセージの生成を容易に行えます。

package main

import (
	"flag"
	"fmt"
)

func main() {
	dbHost := flag.String("host", "127.0.0.1", "データベースのホスト名")
	dbPort := flag.Int("port", 5432, "データベースのポート番号")
	dbUser := flag.String("user", "admin", "接続ユーザー名")
	isDryRun := flag.Bool("dry-run", false, "ドライランモードの有効化")

	flag.Parse()

	fmt.Printf("接続先: %s:%d\n", *dbHost, *dbPort)
	fmt.Printf("ユーザー: %s\n", *dbUser)
	fmt.Printf("ドライラン: %v\n", *isDryRun)
}

JSONのシリアライズとデシリアライズ

Goでは encoding/json パッケージを用いて、構造体やマップとJSON文字列の相互変換を行います。構造体のフィールドにはタグ(json:"key_name")を付与することで、JSONのキー名や出力条件を細かく制御できます。

シリアライズ(エンコード)

Goの構造体をJSONフォーマットのバイトスライスに変換します。

package main

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

type ServerConfig struct {
	HostName   string   `json:"hostname"`
	Port       int      `json:"port"`
	EnableTLS  bool     `json:"enable_tls"`
	AllowedIPs []string `json:"allowed_ips,omitempty"`
}

func main() {
	config := ServerConfig{
		HostName:  "api.example.com",
		Port:      443,
		EnableTLS: true,
	}

	// 構造体をJSONバイトスライスに変換(インデント付き)
	jsonData, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		log.Fatalf("シリアライズエラー: %v", err)
	}

	fmt.Println("生成されたJSON:")
	fmt.Println(string(jsonData))
}

デシリアライズ(デコード)

JSON文字列をGoのデータ構造に復元します。マップやスライスへのデシリアライズも同様のインターフェースで可能です。

package main

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

type User struct {
	ID       int      `json:"id"`
	Username string   `json:"username"`
	Roles    []string `json:"roles"`
}

func main() {
	jsonStr := `{
		"id": 1042,
		"username": "tech_lead",
		"roles": ["admin", "editor"]
	}`

	var user User
	// JSONバイトスライスを構造体にマッピング
	if err := json.Unmarshal([]byte(jsonStr), &user); err != nil {
		log.Fatalf("デシリアライズエラー: %v", err)
	}

	fmt.Printf("ユーザーID: %d\n", user.ID)
	fmt.Printf("ユーザー名: %s\n", user.Username)
	fmt.Printf("権限: %v\n", user.Roles)
}

タグ: Go file-io JSON command-line encoding-json

8月16日 16:21 投稿