Go I/O 的核心規則是:資料來源抽象成 io.Reader,資料目的地抽象成 io.Writer。本章將從檔案讀寫開始,建立 osio 與 streaming API 的基本模型。

檔案操作從 os 開始

os package 的核心責任是處理作業系統層級的資源,例如檔案、目錄、環境變數與 process 相關資訊。入門階段最常用的是讀檔、寫檔與建立檔案。

1data, err := os.ReadFile("config.json")
2if err != nil {
3    return err
4}
5
6fmt.Println(string(data))

os.ReadFile 會一次把整個檔案讀進記憶體,適合設定檔、小型文字檔與測試資料。若檔案可能很大,就應改用 streaming 方式逐步讀取。

寫入小檔案也可以使用 os.WriteFile

1data := []byte("name=demo\n")
2
3if err := os.WriteFile("app.env", data, 0644); err != nil {
4    return err
5}

最後的 0644 是檔案權限。它表示檔案擁有者可讀寫,其他人可讀。權限是 Unix 檔案權限慣例。

開啟檔案後要關閉

檔案是作業系統資源,開啟後應在不使用時關閉。Go 常用 defer file.Close() 放在成功開啟檔案後,確保函式結束時釋放資源。

 1file, err := os.Open("data.txt")
 2if err != nil {
 3    return err
 4}
 5defer file.Close()
 6
 7data, err := io.ReadAll(file)
 8if err != nil {
 9    return err
10}
11
12fmt.Println(string(data))

defer 應該放在確認 err == nil 之後,因為開啟失敗時 file 可能是 nil。這是 Go I/O 程式很重要的基本順序:先檢查錯誤,再使用資源。

io.Reader 表示可讀來源

io.Reader 的核心意義是「可以讀出 bytes 的來源」。檔案、網路連線、HTTP request body、字串 reader 都可以是 reader。

1func countBytes(r io.Reader) (int, error) {
2    data, err := io.ReadAll(r)
3    if err != nil {
4        return 0, err
5    }
6
7    return len(data), nil
8}

這個函式不關心資料來自檔案、記憶體或網路,只要求呼叫端提供一個 io.Reader。這就是 Go 介面設計的典型風格:用小介面描述能力,而不是描述具體來源。

1count, err := countBytes(strings.NewReader("hello"))
2if err != nil {
3    return err
4}
5
6fmt.Println(count)

strings.NewReader 可以把字串包成 reader,常用於測試與範例。因為函式依賴 io.Reader,測試時不需要真的建立檔案。

io.Writer 表示可寫目的地

io.Writer 的核心意義是「可以接收 bytes 的目的地」。檔案、網路連線、HTTP response、記憶體 buffer 都可以是 writer。

1func writeGreeting(w io.Writer, name string) error {
2    _, err := fmt.Fprintf(w, "hello, %s\n", name)
3    return err
4}

這個函式不決定輸出位置,只決定輸出內容。呼叫端可以把內容寫到標準輸出、檔案或 buffer。

1var buffer bytes.Buffer
2
3if err := writeGreeting(&buffer, "alice"); err != nil {
4    return err
5}
6
7fmt.Println(buffer.String())

bytes.Buffer 同時實作 reader 與 writer,適合用來累積輸出或測試寫入結果。

streaming 適合大資料或長連線

streaming 的核心策略是分段處理資料,而不是一次把全部資料載入記憶體。當檔案很大、資料來自網路,或你只需要逐步轉送資料時,streaming 會比 ReadAll 更適合。

 1func copyFile(dstPath string, srcPath string) error {
 2    src, err := os.Open(srcPath)
 3    if err != nil {
 4        return err
 5    }
 6    defer src.Close()
 7
 8    dst, err := os.Create(dstPath)
 9    if err != nil {
10        return err
11    }
12    defer dst.Close()
13
14    _, err = io.Copy(dst, src)
15    return err
16}

io.Copy 從 reader 讀資料並寫到 writer。這段程式沒有手動配置完整檔案大小的 byte slice,因此可以處理比記憶體更大的檔案。

bufio.Scanner 適合逐行讀取

逐行處理文字的核心工具是 bufio.Scanner。它會把 reader 切成一個個 token,預設 token 是一行文字。

1func printLines(r io.Reader) error {
2    scanner := bufio.NewScanner(r)
3    for scanner.Scan() {
4        fmt.Println(scanner.Text())
5    }
6
7    return scanner.Err()
8}

scanner.Scan() 每次成功讀到一行就回傳 true,讀完或遇到錯誤時回傳 false。迴圈結束後要檢查 scanner.Err(),因為讀取錯誤不會在迴圈內直接回傳。

Scanner 適合一般文字行,但它有預設 token 大小限制。若要處理非常長的行或大型二進位資料,應改用 bufio.Reader 或其他 streaming API。

小結

下一章會進入 JSON,說明 Go 如何把 struct 與外部資料格式互相轉換。