3.3 os/io:檔案與輸入輸出
Go I/O 的核心規則是:資料來源抽象成 io.Reader,資料目的地抽象成 io.Writer。本章將從檔案讀寫開始,建立 os、io 與 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 與外部資料格式互相轉換。