ファイル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)
}