本文 AI 產出,尚未審核

Golang – 檔案與 I/O 操作

主題:緩衝 I/O(bufio


簡介

在日常開發中,檔案讀寫網路串流是最常見的 I/O 工作。直接使用 os.FileReadWrite 方法雖然簡單,但每一次系統呼叫都會產生較大的開銷,尤其在大量小資料的情況下,效能會急速下降。

Go 標準函式庫提供的 bufio(buffered I/O)套件,透過在記憶體中建立緩衝區,將多次的系統呼叫合併成一次,從而 提升效能、降低資源消耗。同時,bufio 也提供了許多方便的 API(例如 ScannerReader.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 同時擁有 ReaderWriter 功能 Read, Write, Flush

小技巧:若只需要行為單位的讀取,直接使用 bufio.Scanner 最為簡潔;若需要更彈性的讀寫(例如讀取固定長度、寫入大量資料),則使用 bufio.Reader / bufio.Writer

3. 緩衝大小的選擇

bufio.NewReaderbufio.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.Writerbufio.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.FileRead/Writebufio 若同一 *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 程式的效能與可讀性加分。祝開發順利!