本文 AI 產出,尚未審核

Golang – 檔案與 I/O 操作

讀取與寫入檔案(os、io、ioutil)


簡介

在日常開發中,檔案的讀寫是最常見的 I/O 任務。不論是寫入設定檔、讀取日誌、或是處理大量資料,都離不開對磁碟的操作。Go 語言自帶的 osio 以及已被 ioutil 包裝的便利函式,提供了簡潔且效能良好的檔案處理介面,讓開發者能以最少的程式碼完成複雜的工作。

本篇文章將從 基本概念實作範例常見陷阱與最佳實踐,一步步帶領讀者掌握 Go 中的檔案 I/O。即使是剛接觸 Go 的新手,也能在閱讀完本文後,立即在專案中安全、有效地使用檔案讀寫功能。


核心概念

1. os 套件:檔案的底層操作

os 提供了最原始的檔案描述子(file descriptor)操作,包括開啟、關閉、讀寫、設定權限等。最常用的型別是 *os.File,它同時實作了 io.Readerio.Writerio.Closer 等介面。

1.1 開啟檔案

// os.Open 以唯讀模式開啟檔案,若檔案不存在會回傳錯誤
file, err := os.Open("data.txt")
if err != nil {
    log.Fatalf("開啟檔案失敗: %v", err)
}
defer file.Close() // 確保最後一定關閉

1.2 建立或覆寫檔案

// os.Create 若檔案不存在則建立,若已存在則截斷為空檔
out, err := os.Create("output.log")
if err != nil {
    log.Fatalf("建立檔案失敗: %v", err)
}
defer out.Close()

1.3 讀寫定位(Seek)

// 移動檔案指標到檔案開頭
_, err = file.Seek(0, io.SeekStart)

2. io 套件:抽象化的資料流

io 定義了最小的 I/O 介面(ReaderWriterCloser),以及許多組合工具,如 CopyTeeReaderMultiWriter。使用 io,我們可以在 不同來源與目的地之間自由切換,而不必關心底層是檔案、網路或記憶體。

2.1 io.Copy – 快速複製

src, _ := os.Open("source.bin")
dst, _ := os.Create("dest.bin")
defer src.Close()
defer dst.Close()

// 直接把 src 的內容寫入 dst,返回寫入的位元組數與錯誤
if _, err := io.Copy(dst, src); err != nil {
    log.Fatalf("複製失敗: %v", err)
}

2.2 bufio 包裝(緩衝 I/O)

對於大量小寫入/讀取,使用 bufio.NewWriterbufio.NewReader 能顯著提升效能。

w := bufio.NewWriter(out) // 包裝 *os.File
for i := 0; i < 1000; i++ {
    fmt.Fprintf(w, "第 %d 行資料\n", i)
}
w.Flush() // 必須手動刷新緩衝區

3. ioutil(已棄用但仍常見)與 os 的便利函式

在 Go 1.16 之後,ioutil 大部分功能已移到 osio,但舊程式碼仍大量使用。以下示範兩種寫法,讓讀者了解 新舊 API 的對應關係

3.1 讀取整個檔案

舊版 (ioutil.ReadFile)

data, err := ioutil.ReadFile("config.json")
if err != nil {
    log.Fatalf("讀檔失敗: %v", err)
}

新版 (os.ReadFile)

data, err := os.ReadFile("config.json")
if err != nil {
    log.Fatalf("讀檔失敗: %v", err)
}

3.2 寫入整個檔案

舊版 (ioutil.WriteFile)

content := []byte(`{"mode":"production"}`)
if err := ioutil.WriteFile("settings.json", content, 0644); err != nil {
    log.Fatalf("寫檔失敗: %v", err)
}

新版 (os.WriteFile)

if err := os.WriteFile("settings.json", content, 0644); err != nil {
    log.Fatalf("寫檔失敗: %v", err)
}

4. 完整範例彙總

以下提供 五個實用範例,涵蓋從最簡單的讀寫到緩衝與錯誤處理的完整流程。

4.1 範例一:一次讀取小檔案

package main

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

func main() {
    // 讀取整個檔案內容
    data, err := os.ReadFile("hello.txt")
    if err != nil {
        log.Fatalf("讀檔失敗: %v", err)
    }
    fmt.Printf("檔案內容: %s\n", string(data))
}

重點os.ReadFile 會一次性把檔案載入記憶體,適合 小於 10 MB 的檔案。

4.2 範例二:逐行讀取大型文字檔

package main

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

func main() {
    f, err := os.Open("large.log")
    if err != nil {
        log.Fatalf("開檔失敗: %v", err)
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)
    lineNo := 0
    for scanner.Scan() {
        lineNo++
        // 只顯示前 5 行示範
        if lineNo <= 5 {
            log.Printf("第 %d 行: %s", lineNo, scanner.Text())
        }
    }
    if err := scanner.Err(); err != nil {
        log.Fatalf("讀取過程錯誤: %v", err)
    }
}

技巧bufio.Scanner 內部使用緩衝,能有效處理 GB 級別 的文字檔。

4.3 範例三:寫入大量資料(使用緩衝)

package main

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

func main() {
    f, err := os.Create("numbers.txt")
    if err != nil {
        log.Fatalf("建立檔案失敗: %v", err)
    }
    defer f.Close()

    w := bufio.NewWriter(f)
    for i := 1; i <= 100000; i++ {
        fmt.Fprintf(w, "%d\n", i)
    }
    // 必須 Flush,否則緩衝區的資料不會寫入磁碟
    if err := w.Flush(); err != nil {
        log.Fatalf("Flush 失敗: %v", err)
    }
}

最佳實踐:大量寫入時 一定要呼叫 Flush,否則資料可能遺失。

4.4 範例四:檔案複製(使用 io.Copy

package main

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

func main() {
    src, err := os.Open("source.bin")
    if err != nil {
        log.Fatalf("開啟來源檔失敗: %v", err)
    }
    defer src.Close()

    dst, err := os.Create("dest.bin")
    if err != nil {
        log.Fatalf("建立目的檔失敗: %v", err)
    }
    defer dst.Close()

    // 複製整個檔案
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatalf("複製失敗: %v", err)
    }
}

效能說明io.Copy 內部會根據來源與目的的類型自動選擇最適合的緩衝大小,通常比手寫迴圈更快。

4.5 範例五:安全寫入(使用臨時檔 + 原子替換)

package main

import (
    "io/ioutil"
    "log"
    "os"
)

func main() {
    // 先寫入臨時檔
    tmp, err := ioutil.TempFile("", "config-*.json")
    if err != nil {
        log.Fatalf("建立臨時檔失敗: %v", err)
    }
    defer os.Remove(tmp.Name()) // 若出錯,確保不遺留檔案

    data := []byte(`{"debug":false}`)
    if _, err := tmp.Write(data); err != nil {
        log.Fatalf("寫入臨時檔失敗: %v", err)
    }
    tmp.Close()

    // 原子替換目標檔案
    if err := os.Rename(tmp.Name(), "config.json"); err != nil {
        log.Fatalf("檔案替換失敗: %v", err)
    }
}

安全性:此技巧避免了 寫入過程中斷 導致的檔案損毀,適合 設定檔、資料庫快照 等重要檔案。


常見陷阱與最佳實踐

陷阱 為什麼會發生 解決方式
忘記 defer file.Close() 檔案描述子未釋放,會導致檔案句柄耗盡 每次開檔後立即 defer Close(),即使在迴圈中也要小心避免過多同時開啟的檔案
直接使用 os.OpenFile 而未設定正確權限 權限錯誤會使程式在不同平台上無法寫入 使用 os.FileMode(如 0644)或 os.ModePerm,並根據需求選擇 `O_CREATE
在大量寫入時未使用緩衝 每一次 Write 都會觸發系統呼叫,效能低下 包裝 *os.Filebufio.Writer,寫完後 Flush()
使用 ioutil.ReadAll 讀取過大檔案 會一次性佔用大量記憶體,甚至導致 OOM 改用 bufio.Scannerio.Copy 或分段讀取 (Read + buffer)
錯誤忽略 失敗的 I/O 操作往往會留下隱藏的 bug 每一次 I/O 呼叫後都檢查 err,必要時使用 log.Fatalf 或自訂錯誤處理

最佳實踐總結

  1. 始終檢查錯誤,不要假設檔案一定存在或寫入一定成功。
  2. 使用 defer 關閉檔案,確保資源即時釋放。
  3. 針對不同大小的檔案選擇適當的 API:小檔案 os.ReadFile,大檔案 bufio.Scannerio.Copy
  4. 寫入大量資料時加緩衝,並記得 Flush
  5. 考慮原子寫入(臨時檔 + os.Rename),提升資料完整性。

實際應用場景

場景 需求 推薦的 Go API
設定檔載入 讀取 JSON/YAML,檔案大小通常 < 1 MB os.ReadFile + json.Unmarshal
日誌輪替 持續寫入、每日或檔案大小達到上限時切換 os.OpenFile (O_APPEND) + bufio.Writer
檔案上傳/下載 大檔案 (GB 級) 需要流式傳輸 io.Copy 搭配 net/http Body
資料備份 把資料庫快照寫入磁碟,必須保證完整性 臨時檔 + os.Rename(原子寫入)
文字分析 逐行讀取巨量文字檔,計算關鍵字出現次數 bufio.Scanner + map[string]int

透過上述對應表,開發者可以快速選擇最合適的 I/O 方法,避免「過度抽象」或「過度手寫」的情況。


總結

  • os 提供底層檔案操作,是所有 I/O 的根基。
  • io 抽象出通用的讀寫介面,讓我們可以在不同資料來源間自由切換。
  • ioutil(雖已棄用)仍是舊程式碼的常見寫法,了解其與 os/io 的對應關係有助於維護與升級。

掌握 正確的錯誤處理、資源釋放與緩衝策略,即可在 Go 中寫出 安全、效能佳且易於維護 的檔案 I/O 程式。未來在處理更複雜的資料流(如網路串流、壓縮檔案、雲端儲存)時,這些基礎概念仍是不可或缺的基石。祝你在 Golang 的檔案與 I/O 世界裡玩得開心!