Golang – 檔案與 I/O 操作
主題:緩衝 I/O(bufio)
簡介
在日常開發中,檔案讀寫與網路串流是最常見的 I/O 工作。直接使用 os.File 的 Read、Write 方法雖然簡單,但每一次系統呼叫都會產生較大的開銷,尤其在大量小資料的情況下,效能會急速下降。
Go 標準函式庫提供的 bufio(buffered I/O)套件,透過在記憶體中建立緩衝區,將多次的系統呼叫合併成一次,從而 提升效能、降低資源消耗。同時,bufio 也提供了許多方便的 API(例如 Scanner、Reader.ReadString),讓文字處理變得更直觀。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領讀者掌握 bufio 的使用方式,並提供實務上常見的應用情境,適合 初學者 了解基礎,也能讓 中級開發者 在效能優化時快速上手。
核心概念
1. 為什麼需要緩衝?
- 系統呼叫成本高:每一次
read/write都需要切換到核心模式(kernel mode),耗時較長。 - 小塊資料頻繁讀寫:若一次只讀 1~10 個位元組,CPU 會花大量時間在等待 I/O 完成。
- 緩衝區的作用:一次性讀入較大的資料(如 4KB、8KB),在程式內部再逐筆取用,減少系統呼叫次數。
2. bufio 的主要類型
| 類型 | 功能 | 常用方法 |
|---|---|---|
bufio.Reader |
包裝任意 io.Reader,提供緩衝讀取 |
Read, ReadString, ReadBytes, Peek, Discard |
bufio.Writer |
包裝任意 io.Writer,提供緩衝寫入 |
Write, WriteString, Flush, Reset |
bufio.Scanner |
以行(或自訂分割函式)為單位掃描輸入 | Scan, Text, Bytes, Split, Buffer |
bufio.ReadWriter |
同時擁有 Reader 與 Writer 功能 |
Read, Write, Flush |
小技巧:若只需要行為單位的讀取,直接使用
bufio.Scanner最為簡潔;若需要更彈性的讀寫(例如讀取固定長度、寫入大量資料),則使用bufio.Reader/bufio.Writer。
3. 緩衝大小的選擇
bufio.NewReader、bufio.NewWriter 允許自行指定緩衝大小(預設 4KB)。
- 小檔案:預設即可,因為整個檔案一次就能讀入緩衝。
- 大檔案或網路串流:可根據實際需求調整,例如
64 * 1024(64KB)以減少讀寫次數。 - 注意:過大的緩衝會佔用過多記憶體,特別在高併發服務中要謹慎。
程式碼範例
以下示範 5 個常見且實用的 bufio 用法,均以 完整、可直接執行 的程式為例,並加上說明註解。
範例 1:使用 bufio.Reader 逐行讀取文字檔
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 開啟檔案,若失敗則直接退出
file, err := os.Open("sample.txt")
if err != nil {
panic(err)
}
defer file.Close()
// 建立緩衝讀取器,使用預設 4KB 緩衝
reader := bufio.NewReader(file)
for {
// 讀取到換行符號為止(不含換行符號本身)
line, err := reader.ReadString('\n')
if err != nil {
// io.EOF 代表檔案已讀完
if err.Error() == "EOF" {
// 若最後一行沒有換行,仍要輸出
if len(line) > 0 {
fmt.Print(line)
}
break
}
panic(err)
}
fmt.Print(line) // 直接輸出每一行
}
}
說明:
ReadString('\n')會自動在內部緩衝區累積資料,直到遇到\n為止,極大降低系統呼叫次數。
範例 2:使用 bufio.Scanner 逐行解析 CSV
package main
import (
"bufio"
"encoding/csv"
"fmt"
"os"
)
func main() {
f, err := os.Open("data.csv")
if err != nil {
panic(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
// 逐行掃描
for scanner.Scan() {
line := scanner.Text()
// 使用 csv 讀取器解析單行
r := csv.NewReader(strings.NewReader(line))
record, err := r.Read()
if err != nil {
fmt.Println("parse error:", err)
continue
}
fmt.Printf("欄位數: %d, 第一欄: %s\n", len(record), record[0])
}
if err := scanner.Err(); err != nil {
panic(err)
}
}
重點:
Scanner內建的分割函式是按行(\n),若要自訂分割(例如以\r\n或空白),可呼叫scanner.Split(bufio.ScanWords)。
範例 3:使用 bufio.Writer 寫入大量資料,最後一次性 Flush
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
f, err := os.Create("output.txt")
if err != nil {
panic(err)
}
defer f.Close()
// 建立緩衝寫入器,緩衝大小設為 64KB
writer := bufio.NewWriterSize(f, 64*1024)
for i := 1; i <= 100000; i++ {
// 寫入字串,實際不會立即寫入磁碟
fmt.Fprintf(writer, "第 %d 行資料\n", i)
}
// 必須呼叫 Flush,才能把緩衝區的資料真正寫入檔案
if err := writer.Flush(); err != nil {
panic(err)
}
fmt.Println("寫入完成")
}
提醒:若忘記
Flush(),程式結束時緩衝區仍可能有未寫入的資料,導致檔案內容不完整。
範例 4:同時讀寫(bufio.ReadWriter)— 實作簡易回音伺服器
package main
import (
"bufio"
"fmt"
"net"
)
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
fmt.Println("伺服器已啟動,監聽 8080 埠")
for {
conn, err := ln.Accept()
if err != nil {
continue
}
go handle(conn)
}
}
func handle(c net.Conn) {
defer c.Close()
// 同時包裝 Reader 與 Writer
rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c))
for {
// 讀取客戶端傳來的一行文字
line, err := rw.ReadString('\n')
if err != nil {
return
}
// 回傳相同內容(回音)
_, err = rw.WriteString("Echo: " + line)
if err != nil {
return
}
// 必須 Flush,才能把資料送回客戶端
rw.Flush()
}
}
實務意義:在網路程式中,
ReadWriter可以一次性管理緩衝,減少重複建立Reader/Writer的成本。
範例 5:自訂 Scanner 的緩衝上限,避免「Token too long」錯誤
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
f, err := os.Open("bigline.txt")
if err != nil {
panic(err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
// 預設緩衝上限為 64KB,若檔案中有超長行會 panic
const maxCapacity = 10 * 1024 * 1024 // 10 MB
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
for scanner.Scan() {
fmt.Println("讀到一行長度:", len(scanner.Bytes()))
}
if err := scanner.Err(); err != nil {
panic(err)
}
}
技巧:
Scanner只適合處理「行」或「字」等較小的 token,若要讀取巨大的單行,請改用bufio.Reader.ReadString或自行管理緩衝。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記 Flush() |
bufio.Writer、bufio.ReadWriter 的資料會暫留在記憶體,程式結束或錯誤時未寫入檔案/網路。 |
一定在寫入完成後呼叫 Flush(),或使用 defer writer.Flush() 確保執行。 |
| 緩衝區過小 | 讀寫大量資料時,過小的緩衝會導致頻繁的系統呼叫,抵消 bufio 的效益。 |
根據資料量調整緩衝大小,例如 bufio.NewWriterSize(w, 64*1024)。 |
使用 Scanner 處理大檔案 |
Scanner 內部緩衝上限預設 64KB,超過會拋出 token too long。 |
使用 scanner.Buffer(make([]byte, max), max) 調整上限,或改用 Reader。 |
同時使用 os.File 的 Read/Write 與 bufio |
若同一 *os.File 同時被原始 I/O 與緩衝 I/O 操作,會產生資料不一致。 |
只使用其中一種方式,或在切換前呼叫 file.Sync() 與 bufio.Writer.Flush()。 |
| 忘記關閉檔案 | defer file.Close() 必不可少,否則檔案描述符會泄漏。 |
養成在開檔後立即 defer file.Close() 的好習慣。 |
最佳實踐小結
- 預設使用
bufio.Scanner讀取行,簡潔且安全。 - 大量寫入時使用
bufio.Writer並自行設定緩衝大小,最後務必Flush()。 - 網路程式 建議使用
bufio.ReadWriter,一次性管理讀寫緩衝。 - 測試緩衝大小:在效能測試(benchmark)中比較不同緩衝設定,找出最佳點。
- 錯誤處理:所有 I/O 操作皆可能返回錯誤,務必檢查
err,尤其是Flush()、Scan()、ReadString()。
實際應用場景
| 場景 | 為什麼需要 bufio |
典型程式碼片段 |
|---|---|---|
| 日誌檔寫入 | 高頻率的 log.Println 會頻繁寫磁碟,使用緩衝可一次寫入多筆。 |
writer := bufio.NewWriterSize(logFile, 128*1024) |
| CSV/TSV 大檔案匯入 | 逐行解析需要大量 ReadString('\n'),緩衝減少磁碟 I/O。 |
scanner := bufio.NewScanner(csvFile) |
| HTTP 代理服務 | 讀取客戶端請求與回傳伺服器回應,使用 ReadWriter 可降低延遲。 |
rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) |
| 資料備份工具 | 讀取來源檔案、寫入目標檔案,緩衝提升 2~5 倍的寫入速度。 | io.Copy(bufio.NewWriter(dst), bufio.NewReader(src)) |
| 即時聊天系統 | 每條訊息皆為短字串,使用 Scanner 逐行讀取,配合 Flush 即可即時回傳。 |
參考範例 4(回音伺服器) |
總結
bufio 是 Go 語言中 提升 I/O 效能的關鍵工具,它透過在記憶體中建立緩衝區,將多次系統呼叫合併,讓檔案、網路、標準輸入/輸出等操作變得更快、更省資源。掌握以下要點,即可在日常開發中善用 bufio:
- 選對類型:行為單位使用
Scanner,彈性讀寫使用Reader/Writer,同時讀寫使用ReadWriter。 - 適當設定緩衝大小:根據資料量與併發需求調整,避免過小或過大。
- 務必
Flush():寫入緩衝後一定要把資料送出,否則檔案或網路資料會遺失。 - 留意常見陷阱:如
Scanner的 token 長度限制、混用原始 I/O 與緩衝 I/O。
透過本文的概念說明與實作範例,讀者應已能在 檔案處理、日誌寫入、網路服務 等多種情境中,熟練且安全地使用 bufio,為 Go 程式的效能與可讀性加分。祝開發順利!