本文 AI 產出,尚未審核

Golang – 檔案與 I/O 操作

單元:目錄操作(filepath, os


簡介

在日常開發中,目錄(Directory)的管理往往是不可或缺的工作。無論是建立日誌資料夾、整理下載檔案,或是實作簡易的檔案伺服器,都需要對目錄進行建立、列舉、遍歷、刪除等操作。Go 語言提供了兩個核心套件——osfilepath,分別負責底層的檔案系統呼叫與跨平台的路徑處理,讓開發者可以以簡潔且安全的方式完成目錄相關的需求。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領讀者掌握 目錄操作 的技巧,並提供幾個實務應用情境,幫助你在專案中快速上手、寫出可維護的程式碼。


核心概念

1. os 套件與目錄的基本操作

函式 功能 重要回傳值
os.Mkdir(name string, perm os.FileMode) 建立單層目錄 error
os.MkdirAll(path string, perm os.FileMode) 建立多層目錄(若已存在則略過) error
os.Remove(name string) 刪除檔案或空目錄 error
os.RemoveAll(path string) 遞迴刪除目錄(含子目錄與檔案) error
os.ReadDir(name string) 讀取目錄內容,回傳 []os.DirEntry error
os.Stat(name string) 取得檔案或目錄資訊(os.FileInfo error

os.FileMode 采用 Unix 權限位元(如 0755),在 Windows 上會自動映射為相容的 ACL。

2. filepath 套件的跨平台路徑處理

函式 功能 範例
filepath.Join(elem ...string) 合併路徑片段,自動使用正確的分隔符(/\ filepath.Join("logs", "2024", "12")logs/2024/12
filepath.Abs(path string) 取得絕對路徑 filepath.Abs("./data")
filepath.Clean(path string) 正規化路徑(去除 ..、多餘的分隔符) filepath.Clean("/a//b/../c")/a/c
filepath.WalkDir(root string, fn fs.WalkDirFunc) 遞迴遍歷目錄(支援 fs.DirEntry 參考範例 4
filepath.Rel(basepath, targpath string) 計算兩路徑的相對路徑 filepath.Rel("/a/b", "/a/b/c/d")c/d

filepath 主要負責 路徑字串的組合、正規化與查詢,而實際的檔案系統操作則交給 os(或 io/fs)完成。將兩者結合使用,可寫出既跨平台安全的程式。

3. 權限與錯誤處理

  • 權限:在 Unix 系統上,os.FileMode 的前三位代表 所有者、群組、其他使用者 的讀寫執行權限。常見的目錄權限為 0755(所有者可寫,其他人只能讀取)。
  • 錯誤類型os.IsNotExist(err)os.IsExist(err)os.IsPermission(err) 等輔助函式,可協助判斷錯誤原因,避免因未處理的例外導致程式崩潰。

程式碼範例

以下示範 5 個常見且實用的目錄操作範例,皆以 完整、可直接執行 的程式片段呈現。

範例 1️⃣ 建立多層目錄(os.MkdirAll

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func main() {
	// 目標路徑:logs/2024/12/19
	dir := filepath.Join("logs", "2024", "12", "19")

	// 權限 0755:所有者可寫,其他人只能讀取
	if err := os.MkdirAll(dir, 0o755); err != nil {
		fmt.Printf("建立目錄失敗: %v\n", err)
		return
	}
	fmt.Printf("成功建立目錄:%s\n", dir)
}

說明

  • filepath.Join 確保在 Windows、Linux、macOS 上都會得到正確的分隔符。
  • os.MkdirAll 若目錄已存在,會直接返回 nil,不會拋出錯誤。

範例 2️⃣ 讀取目錄內容(os.ReadDir

package main

import (
	"fmt"
	"os"
)

func main() {
	entries, err := os.ReadDir("./logs")
	if err != nil {
		fmt.Printf("讀取目錄失敗: %v\n", err)
		return
	}

	fmt.Println("logs 目錄下的檔案與子目錄:")
	for _, e := range entries {
		// 判斷是否為目錄
		if e.IsDir() {
			fmt.Printf("[DIR]  %s\n", e.Name())
		} else {
			fmt.Printf("[FILE] %s\n", e.Name())
		}
	}
}

說明

  • os.ReadDir 回傳 []os.DirEntry,比起舊版的 os.FileInfo 更有效率,因為它不會一次性取得檔案的全部資訊。

範例 3️⃣ 刪除非空目錄(os.RemoveAll

package main

import (
	"fmt"
	"os"
)

func main() {
	target := "./tmp/old_data"

	// 確認目錄是否真的存在,避免誤刪除
	if _, err := os.Stat(target); os.IsNotExist(err) {
		fmt.Printf("目錄不存在:%s\n", target)
		return
	}

	if err := os.RemoveAll(target); err != nil {
		fmt.Printf("刪除失敗:%v\n", err)
		return
	}
	fmt.Printf("已遞迴刪除目錄:%s\n", target)
}

說明

  • os.RemoveAll 會遞迴刪除子目錄與檔案,使用前務必確認路徑正確,以免誤刪重要資料。

範例 4️⃣ 遞迴遍歷目錄(filepath.WalkDir

package main

import (
	"fmt"
	"io/fs"
	"path/filepath"
)

func main() {
	root := "./logs"

	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			// 若讀取某個子目錄失敗,直接回傳錯誤結束遍歷
			return err
		}
		// 只列出 .log 結尾的檔案
		if !d.IsDir() && filepath.Ext(d.Name()) == ".log" {
			fmt.Println("找到 log 檔案:", path)
		}
		return nil // 繼續遍歷
	})

	if err != nil {
		fmt.Printf("遍歷失敗:%v\n", err)
	}
}

說明

  • WalkDir 使用 fs.DirEntry,效能較 filepath.Walk 更好。
  • 透過 filepath.Ext 只挑選特定副檔名的檔案,常用於日誌或資料清理腳本。

範例 5️⃣ 取得相對路徑與絕對路徑(filepath.Relfilepath.Abs

package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	// 假設當前工作目錄是 /home/user/project
	absPath, _ := filepath.Abs("./logs/2024/12/19")
	fmt.Println("絕對路徑:", absPath)

	rel, err := filepath.Rel("/home/user", absPath)
	if err != nil {
		fmt.Printf("計算相對路徑失敗: %v\n", err)
		return
	}
	fmt.Println("相對於 /home/user 的路徑:", rel)
}

說明

  • filepath.Abs 可將相對路徑轉為絕對路徑,常在日誌寫入前先做一次正規化。
  • filepath.Rel 讓你在多層目錄結構中產生相對路徑,對於產生可搬移的設定檔或報告非常有用。

常見陷阱與最佳實踐

陷阱 可能的後果 解決方式
硬編碼路徑分隔符(使用 /\ 在不同 OS 上執行會失敗 使用 filepath.Joinfilepath.Separator
未檢查 os.Mkdir/os.MkdirAll 的錯誤 目錄未建立卻繼續寫入檔案,導致 panic 永遠檢查 error,必要時使用 os.IsExist 判斷是否已存在
刪除目錄時忘記 os.RemoveAll 的危險性 誤刪重要資料 在刪除前確認路徑,或使用 dry-run 模式列印將要刪除的項目
遍歷大量檔案時未使用 WalkDir 記憶體占用過高、效能下降 使用 filepath.WalkDir 搭配 fs.SkipDir 早期退出
忽略權限錯誤os.IsPermission 程式在特定環境(如容器)崩潰 針對權限錯誤提供回退機制或提示使用者提升權限

推薦的程式撰寫風格

  1. 統一使用 filepath 處理路徑:即使在 Windows 開發,也不必自行判斷分隔符。
  2. 錯誤即時處理:使用 if err != nil { return fmt.Errorf("...: %w", err) } 包裝錯誤,保留堆疊資訊。
  3. 使用 defer 釋放資源:雖然目錄本身不需要關閉,但在遍歷過程中若開啟檔案,務必 defer file.Close()
  4. 避免硬編碼權限:使用 os.FileMode 常數或 0o 前綴(Go 1.13+),讓程式更易讀。

實際應用場景

場景 需求 相關函式 範例說明
日誌輪替(Log Rotation) 每天自動產生新目錄、刪除超過 30 天的舊目錄 os.MkdirAll, filepath.WalkDir, os.RemoveAll 先用 time.Now().Format("2006-01-02") 建立目錄,再遍歷 logs/ 刪除過期目錄。
檔案上傳服務 上傳檔案前先建立使用者專屬目錄,確保路徑安全 filepath.Join, os.MkdirAll, os.Stat userID 為子目錄,若不存在即建立,避免路徑穿越攻擊。
備份與還原工具 需要將指定目錄遞迴壓縮、解壓縮,並保留相對路徑 filepath.WalkDir, os.Create, archive/zip 先遍歷目錄取得相對路徑,再寫入 zip 檔。
跨平台 CLI 工具 讀取配置檔所在目錄,支援 Windows、macOS、Linux filepath.Abs, os.UserHomeDir 先取得使用者主目錄,再 Join 產生 ~/.mycli/config.yaml 的絕對路徑。
容器化部署腳本 在容器啟動時自動建立掛載卷的子目錄 os.MkdirAll, os.Chmod 使用 0755 權限建立 /data/logs,確保容器內程式可寫入。

總結

目錄操作是 檔案 I/O 中最常見也最基礎的部分。透過 os 套件,我們可以完成建立、刪除、列舉等底層動作;而 filepath 則負責 跨平台路徑組合與正規化,兩者結合即可寫出安全、可移植的程式碼。

本文重點回顧:

  • 使用 filepath.Join 取代手動字串拼接,避免平台差異。
  • os.MkdirAll 能一次建立多層目錄,配合適當的 FileMode
  • 遍歷目錄 建議使用 filepath.WalkDir,效能較佳且支援 fs.SkipDir
  • 刪除目錄 必須小心 os.RemoveAll,務必在執行前確認路徑。
  • 錯誤處理 不容忽視,os.IsNotExistos.IsPermission 等輔助函式能讓程式更健壯。

掌握以上概念與範例後,你就能在日誌管理、檔案上傳、備份工具等多種情境下,快速且可靠地處理目錄相關的需求。祝你在 Golang 的檔案與 I/O 世界裡玩得開心,寫出更乾淨、更可維護的程式碼!