本文 AI 產出,尚未審核
Golang – 檔案與 I/O 操作
單元:目錄操作(filepath, os)
簡介
在日常開發中,目錄(Directory)的管理往往是不可或缺的工作。無論是建立日誌資料夾、整理下載檔案,或是實作簡易的檔案伺服器,都需要對目錄進行建立、列舉、遍歷、刪除等操作。Go 語言提供了兩個核心套件——os 與 filepath,分別負責底層的檔案系統呼叫與跨平台的路徑處理,讓開發者可以以簡潔且安全的方式完成目錄相關的需求。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領讀者掌握 目錄操作 的技巧,並提供幾個實務應用情境,幫助你在專案中快速上手、寫出可維護的程式碼。
核心概念
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.Rel、filepath.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.Join、filepath.Separator |
未檢查 os.Mkdir/os.MkdirAll 的錯誤 |
目錄未建立卻繼續寫入檔案,導致 panic | 永遠檢查 error,必要時使用 os.IsExist 判斷是否已存在 |
刪除目錄時忘記 os.RemoveAll 的危險性 |
誤刪重要資料 | 在刪除前確認路徑,或使用 dry-run 模式列印將要刪除的項目 |
遍歷大量檔案時未使用 WalkDir |
記憶體占用過高、效能下降 | 使用 filepath.WalkDir 搭配 fs.SkipDir 早期退出 |
忽略權限錯誤(os.IsPermission) |
程式在特定環境(如容器)崩潰 | 針對權限錯誤提供回退機制或提示使用者提升權限 |
推薦的程式撰寫風格
- 統一使用
filepath處理路徑:即使在 Windows 開發,也不必自行判斷分隔符。 - 錯誤即時處理:使用
if err != nil { return fmt.Errorf("...: %w", err) }包裝錯誤,保留堆疊資訊。 - 使用
defer釋放資源:雖然目錄本身不需要關閉,但在遍歷過程中若開啟檔案,務必defer file.Close()。 - 避免硬編碼權限:使用
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.IsNotExist、os.IsPermission等輔助函式能讓程式更健壯。
掌握以上概念與範例後,你就能在日誌管理、檔案上傳、備份工具等多種情境下,快速且可靠地處理目錄相關的需求。祝你在 Golang 的檔案與 I/O 世界裡玩得開心,寫出更乾淨、更可維護的程式碼!