本文 AI 產出,尚未審核

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() 的好習慣。

最佳實踐小結

  1. 預設使用 bufio.Scanner 讀取行,簡潔且安全。
  2. 大量寫入時使用 bufio.Writer 並自行設定緩衝大小,最後務必 Flush()。
  3. 網路程式 建議使用 bufio.ReadWriter,一次性管理讀寫緩衝。
  4. 測試緩衝大小:在效能測試(benchmark)中比較不同緩衝設定,找出最佳點。
  5. 錯誤處理:所有 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:

  1. 選對類型:行為單位使用 Scanner,彈性讀寫使用 Reader / Writer,同時讀寫使用 ReadWriter。
  2. 適當設定緩衝大小:根據資料量與併發需求調整,避免過小或過大。
  3. 務必 Flush():寫入緩衝後一定要把資料送出,否則檔案或網路資料會遺失。
  4. 留意常見陷阱:如 Scanner 的 token 長度限制、混用原始 I/O 與緩衝 I/O。

透過本文的概念說明與實作範例,讀者應已能在 檔案處理、日誌寫入、網路服務 等多種情境中,熟練且安全地使用 bufio,為 Go 程式的效能與可讀性加分。祝開發順利!