Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Go path/filepath 包详解

概述

path/filepath 包实现了用于操作文件名路径的实用函数,兼容目标操作系统定义的文件路径。

重要说明

  • ✓ 操作系统文件路径操作
  • ✓ 自动使用正确的路径分隔符
  • ✓ Go 1.0+ 引入
  • ✓ 访问文件系统
  • ✓ 支持 Windows 和 Unix 路径
  • ✓ 解析符号链接
  • ✓ 文件模式匹配和遍历

重要区别

  • path 包:仅用于斜杠分隔的路径(如 URL 路径)
  • path/filepath 包:用于操作系统文件路径(Windows 使用反斜杠,Unix 使用正斜杠)

路径分隔符

  • Windows:反斜杠 \
  • Unix/Linux/macOS:正斜杠 /

包导入

import (
    "path/filepath"
)

基本使用

1. 路径清理(跨平台)

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    dirty := "/a/b/../c/./d"
    clean := filepath.Clean(dirty)
    fmt.Printf("清理前:%s\n", dirty)
    fmt.Printf("清理后:%s\n", clean)
}

运行结果(Unix):

清理前:/a/b/../c/./d
清理后:/a/c/d

2. 路径连接(跨平台)

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    full := filepath.Join("home", "user", "documents")
    fmt.Printf("连接结果:%s\n", full)
}

运行结果:

Unix: home/user/documents
Windows: home\user\documents

3. 获取绝对路径

package main

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

func main() {
    abs, err := filepath.Abs("relative/path")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("绝对路径:%s\n", abs)
}

一、常量

Separator

const Separator = os.PathSeparator

Separator 是操作系统特定的路径分隔符。

值:

  • Windows: \ (92)
  • Unix/Linux/macOS: / (47)

示例:

fmt.Println(filepath.Separator)
// Windows: \
// Unix: /

ListSeparator

const ListSeparator = os.PathListSeparator

ListSeparator 是路径列表的分隔符(用于 PATH 等环境变量)。

值:

  • Windows: ;
  • Unix/Linux/macOS: :

示例:

// Unix: /usr/bin:/usr/local/bin
// Windows: C:\Windows;C:\Windows\System32

二、变量

ErrBadPattern

var ErrBadPattern = errors.New("path: malformed pattern")

ErrBadPattern 表示模式格式错误。

说明:

  • 当 Match 或 Glob 函数的 pattern 参数格式不正确时返回
  • 常见的错误原因:
    • 字符类未闭合:"file[.txt"
    • 转义字符在末尾:"file\"
    • 空的字符类:"file[]"

示例:

_, err := filepath.Glob("file[")
if err == filepath.ErrBadPattern {
    fmt.Println("模式格式错误")
}

SkipDir

var SkipDir error = fs.SkipDir

SkipDir 用作 WalkFunc 的返回值,指示跳过当前目录。

说明:

  • 不是错误,是控制 Walk 行为的特殊返回值
  • 当函数返回 SkipDir 时,Walk 跳过当前目录

示例:

filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
    if info.IsDir() && info.Name() == ".git" {
        return filepath.SkipDir // 跳过 .git 目录
    }
    return nil
})

SkipAll

var SkipAll error = fs.SkipAll

SkipAll 用作 WalkFunc 的返回值,指示跳过所有剩余文件和目录。

说明:

  • Go 1.20+ 引入
  • 不是错误,是控制 Walk 行为的特殊返回值
  • 当函数返回 SkipAll 时,Walk 立即停止

示例:

count := 0
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
    count++
    if count >= 100 {
        return filepath.SkipAll // 限制处理 100 个文件
    }
    return nil
})

三、类型

WalkFunc

WalkFunc 是 Walk 调用的函数类型。

type WalkFunc func(path string, info os.FileInfo, err error) error

参数:

  • path - 文件或目录的路径
  • info - 文件信息(如果 err 非 nil 可能为 nil)
  • err - 访问路径时的错误

返回值控制 Walk 行为:

  • nil - 继续遍历
  • SkipDir - 跳过当前目录
  • SkipAll - 跳过所有剩余(Go 1.20+)
  • 其他错误 - 停止遍历并返回该错误

示例:

walkFn := func(path string, info os.FileInfo, err error) error {
    if err != nil {
        fmt.Printf("访问错误:%v\n", err)
        return err
    }
    
    fmt.Printf("访问:%s (%s)\n", path, info.Name())
    
    // 跳过特定目录
    if info.IsDir() && info.Name() == "vendor" {
        return filepath.SkipDir
    }
    
    return nil
}

filepath.Walk(".", walkFn)

四、函数(按 a-z 排序)

Abs

func Abs(path string) (string, error)

Abs 返回 path 的绝对表示形式。

参数:

  • path - 路径(可以是相对路径或绝对路径)

返回值:

  • string - 绝对路径
  • error - 错误

处理规则:

  • 如果 path 不是绝对路径,将与当前工作目录连接
  • 给定文件的绝对路径名不保证唯一(可能有符号链接)
  • 调用 Clean 清理结果

示例:

abs, err := filepath.Abs("relative/path")
if err != nil {
    log.Fatal(err)
}
fmt.Println(abs)
// Unix: /home/user/relative/path
// Windows: C:\Users\user\relative\path

使用场景:

  • 将相对路径转换为绝对路径
  • 规范化路径表示

Base

func Base(path string) string

Base 返回 path 的最后一个元素。

参数:

  • path - 输入路径

返回值:

  • string - 最后一个元素(文件名)

处理规则:

  • 在提取最后一个元素之前,删除尾随的路径分隔符
  • 如果路径为空,返回 “.”
  • 如果路径完全由分隔符组成,返回单个分隔符

示例:

fmt.Println(filepath.Base("/home/user/file.txt"))
// Unix: file.txt
// Windows: file.txt

fmt.Println(filepath.Base("/home/user/"))
// user

fmt.Println(filepath.Base("/"))
// / (Unix)
// \ (Windows)

fmt.Println(filepath.Base(""))
// .

Clean

func Clean(path string) string

Clean 通过纯词法处理返回等价于 path 的最短路径名。

参数:

  • path - 输入路径

返回值:

  • string - 清理后的路径

处理规则(迭代应用):

  1. 将多个分隔符替换为单个
  2. 消除每个 . 元素(当前目录)
  3. 消除每个内部的 .. 元素及其前面的非 .. 元素
  4. 消除以根路径开头的 .. 元素
  5. 返回的路径仅在根目录时以分隔符结尾
  6. 将斜杠替换为操作系统分隔符
  7. 如果结果为空,返回 “.”

示例:

fmt.Println(filepath.Clean("/a/b/../c"))
// Unix: /a/c
// Windows: \a\c

fmt.Println(filepath.Clean("/a//b"))
// Unix: /a/b

fmt.Println(filepath.Clean(""))
// .

fmt.Println(filepath.Clean("/../a"))
// Unix: /a

Windows 特殊处理:

// Windows 上,卷标名只替换斜杠
filepath.Clean("//host/share/../x")
// 返回:\\host\share\x

Dir

func Dir(path string) string

Dir 返回 path 除最后一个元素之外的所有部分,通常是路径的目录。

参数:

  • path - 输入路径

返回值:

  • string - 目录路径

处理规则:

  • 删除最后一个元素后调用 Clean
  • 删除尾随分隔符
  • 如果路径为空,返回 “.”
  • 如果路径完全由分隔符组成,返回单个分隔符
  • 返回的路径不以分隔符结尾(除非是根目录)

示例:

fmt.Println(filepath.Dir("/home/user/file.txt"))
// Unix: /home/user
// Windows: \home\user

fmt.Println(filepath.Dir("/"))
// Unix: /
// Windows: \

fmt.Println(filepath.Dir(""))
// .
func EvalSymlinks(path string) (string, error)

EvalSymlinks 返回评估任何符号链接后的路径名。

参数:

  • path - 输入路径(可以包含符号链接)

返回值:

  • string - 解析后的路径
  • error - 错误(如果路径不存在或无法解析)

说明:

  • 如果 path 是相对路径,结果将相对于当前目录
  • 除非某个组件是绝对符号链接
  • 调用 Clean 清理结果
  • 访问文件系统

示例:

// 假设 /tmp/link -> /home/user
realPath, err := filepath.EvalSymlinks("/tmp/link/file.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(realPath)
// 输出:/home/user/file.txt

使用场景:

  • 解析符号链接获取真实路径
  • 规范化路径(包括符号链接)

Ext

func Ext(path string) string

Ext 返回 path 使用的文件扩展名。

参数:

  • path - 输入路径

返回值:

  • string - 文件扩展名(包括点)

处理规则:

  • 扩展名从 path 最后一个元素的最后一个点开始
  • 如果没有点,返回空字符串
  • 扩展名包括点前缀

示例:

fmt.Println(filepath.Ext("/home/user/file.txt"))
// .txt

fmt.Println(filepath.Ext("file.tar.gz"))
// .gz

fmt.Println(filepath.Ext("/home/user/file"))
// ""

fmt.Println(filepath.Ext(".bashrc"))
// ""

FromSlash

func FromSlash(path string) string

FromSlash 将 path 中的每个斜杠字符替换为分隔符字符。

参数:

  • path - 斜杠分隔的路径

返回值:

  • string - 操作系统路径

说明:

  • 多个斜杠被多个分隔符替换
  • 用于将 io/fs 包使用的斜杠路径转换为操作系统路径

示例:

path := "home/user/documents"
osPath := filepath.FromSlash(path)
fmt.Println(osPath)
// Unix: home/user/documents
// Windows: home\user\documents

Glob

func Glob(pattern string) (matches []string, err error)

Glob 返回匹配模式的所有文件名称,如果没有匹配文件则返回 nil。

参数:

  • pattern - 模式(语法与 Match 相同)

返回值:

  • []string - 匹配的文件名列表
  • error - 错误(仅当模式错误时返回 ErrBadPattern)

说明:

  • 模式可以描述分层名称,如 /usr/*/bin/ed
  • 忽略文件系统错误(如读取目录的 I/O 错误)
  • 按字典顺序返回结果

示例:

// 查找所有 .go 文件
matches, err := filepath.Glob("*.go")
if err != nil {
    log.Fatal(err)
}
fmt.Println(matches)
// [main.go util.go config.go]

// 查找子目录中的文件
matches, err = filepath.Glob("docs/*.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(matches)
// [docs/readme.txt docs/notes.txt]

// 使用 ** 递归查找(某些 shell 支持)
matches, err = filepath.Glob("**/*.go")
// 注意:Go 的 Glob 不支持 **,需要手动实现

错误处理:

_, err := filepath.Glob("file[")
if err == filepath.ErrBadPattern {
    fmt.Println("模式格式错误")
}

HasPrefix(已弃用)

func HasPrefix(p, prefix string) bool

已弃用:HasPrefix 不考虑路径边界,需要时也不忽略大小写。

不要使用此函数,使用其他方法检查路径前缀。

IsAbs

func IsAbs(path string) bool

IsAbs 报告路径是否为绝对路径。

参数:

  • path - 输入路径

返回值:

  • bool - 如果是绝对路径返回 true

判断规则:

  • Unix:以 / 开头
  • Windows:以 \ 开头、包含卷标(如 C:)、或 UNC 路径(如 \\server\share

示例:

// Unix
fmt.Println(filepath.IsAbs("/home/user"))  // true
fmt.Println(filepath.IsAbs("home/user"))   // false
fmt.Println(filepath.IsAbs("./file"))      // false

// Windows
fmt.Println(filepath.IsAbs("C:\\Windows")) // true
fmt.Println(filepath.IsAbs("\\Windows"))   // true
fmt.Println(filepath.IsAbs("C:file.txt"))  // false (相对路径)

IsLocal

func IsLocal(path string) bool

IsLocal 报告 path 是否满足以下所有属性(仅词法分析):

  • 位于 path 评估的目录根子树内
  • 不是绝对路径
  • 不是空路径
  • Windows 上,不是保留名称(如 “NUL”)

参数:

  • path - 输入路径

返回值:

  • bool - 如果是本地路径返回 true

说明:

  • Go 1.20+ 引入
  • 纯词法操作,不考虑符号链接
  • 如果 IsLocal(path) 返回 true,则 Join(base, path) 始终产生包含在 base 内的路径

示例:

fmt.Println(filepath.IsLocal("file.txt"))      // true
fmt.Println(filepath.IsLocal("dir/file.txt"))  // true
fmt.Println(filepath.IsAbs("/file.txt"))       // false (绝对路径)
fmt.Println(filepath.IsLocal(""))              // false (空路径)
fmt.Println(filepath.IsLocal(".."))            // false (包含 ..)
fmt.Println(filepath.IsLocal("dir/../file"))   // false (包含 ..)

Join

func Join(elem ...string) string

Join 将任意数量的路径元素连接成单个路径,用操作系统特定的分隔符分隔。

参数:

  • elem - 路径元素的可变参数

返回值:

  • string - 连接后的路径(已清理)

处理规则:

  • 用操作系统分隔符分隔各个元素
  • 忽略空元素
  • 结果会被 Clean 清理
  • 如果参数列表为空或所有元素都为空,返回空字符串
  • Windows 上,仅当第一个非空元素是 UNC 路径时,结果才是 UNC 路径

示例:

fmt.Println(filepath.Join("home", "user", "docs"))
// Unix: home/user/docs
// Windows: home\user\docs

fmt.Println(filepath.Join("/home", "user", "docs"))
// Unix: /home/user/docs
// Windows: \home\user\docs

fmt.Println(filepath.Join())
// ""

fmt.Println(filepath.Join("", ""))
// ""

fmt.Println(filepath.Join("a", "../b"))
// Unix: ../b (不是 b)

Windows UNC 路径:

fmt.Println(filepath.Join("\\\\host\\share", "file.txt"))
// \\host\share\file.txt

Localize

func Localize(path string) (string, error)

Localize 将斜杠分隔的路径转换为操作系统路径。

参数:

  • path - 斜杠分隔的路径(必须是 io/fs.ValidPath 报告的有效路径)

返回值:

  • string - 操作系统路径
  • error - 如果路径无法由操作系统表示

说明:

  • Go 1.20+ 引入
  • 输入路径必须是有效的 io/fs 路径
  • 返回的路径始终是本地路径(IsLocal 返回 true)

示例:

osPath, err := filepath.Localize("home/user/docs")
if err != nil {
    log.Fatal(err)
}
fmt.Println(osPath)
// Unix: home/user/docs
// Windows: home\user\docs

// 错误示例
_, err = filepath.Localize("a\b") // Windows 上错误
// \ 是分隔符,不能是文件名的一部分

Match

func Match(pattern, name string) (matched bool, err error)

Match 报告 name 是否匹配 shell 文件名模式。

参数:

  • pattern - 模式字符串
  • name - 要匹配的名称

返回值:

  • bool - 是否匹配
  • error - 错误(仅当模式错误时)

模式语法:

pattern:
    { term }
term:
    '*'         匹配任何非分隔符字符序列
    '?'         匹配任何单个非分隔符字符
    '[' [ '^' ] { character-range } ']'
                字符类(必须非空)
    c           匹配字符 c (c != '*', '?', '\', '[')
    '\' c       匹配字符 c

character-range:
    c           匹配字符 c (c != '\', '-', ']')
    '\' c       匹配字符 c
    lo '-' hi   匹配字符 c (lo <= c <= hi)

示例:

matched, _ := filepath.Match("*.go", "main.go")
fmt.Println(matched)  // true

matched, _ = filepath.Match("*.go", "main.txt")
fmt.Println(matched)  // false

matched, _ = filepath.Match("file?.txt", "file1.txt")
fmt.Println(matched)  // true

matched, _ = filepath.Match("file[0-9].txt", "file5.txt")
fmt.Println(matched)  // true

matched, _ = filepath.Match("file[!0-9].txt", "filea.txt")
fmt.Println(matched)  // true

Windows 特殊性:

// Windows 上,转义被禁用
// \ 被视为路径分隔符
matched, _ = filepath.Match("file\\test.txt", "file\\test.txt")
// Windows: 可能不工作,\ 是分隔符

Rel

func Rel(basepath, targpath string) (string, error)

Rel 返回一个相对路径,当与 basepath 连接时,词法上等价于 targpath。

参数:

  • basepath - 基础路径
  • targpath - 目标路径

返回值:

  • string - 相对路径
  • error - 错误

说明:

  • Join(basepath, Rel(basepath, targpath)) 等价于 targpath
  • 成功时,返回的路径始终相对于 basepath
  • 即使 basepath 和 targpath 没有共同元素
  • 如果无法计算相对路径或需要当前工作目录,返回错误
  • 调用 Clean 清理结果

示例:

rel, err := filepath.Rel("/home/user", "/home/user/docs/file.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(rel)
// Unix: docs/file.txt

rel, err = filepath.Rel("/home/user", "/var/log/app.log")
if err != nil {
    log.Fatal(err)
}
fmt.Println(rel)
// Unix: ../../var/log/app.log

错误情况:

// 无法计算相对路径
_, err := filepath.Rel("/a", "./b/c")
// 错误:can't make ./b/c relative to /a

Split

func Split(path string) (dir, file string)

Split 在最后一个分隔符后立即分割 path,将其分为目录和文件名组件。

参数:

  • path - 输入路径

返回值:

  • dir - 目录部分(包括最后的分隔符)
  • file - 文件名部分

处理规则:

  • 如果 path 中没有分隔符,返回空的 dir 和设置为 path 的 file
  • 返回值满足:path = dir + file

示例:

dir, file := filepath.Split("/home/user/file.txt")
fmt.Printf("dir: %q, file: %q\n", dir, file)
// Unix: dir: "/home/user/", file: "file.txt"

dir, file = filepath.Split("file.txt")
fmt.Printf("dir: %q, file: %q\n", dir, file)
// dir: "", file: "file.txt"

dir, file = filepath.Split("/home/user/")
fmt.Printf("dir: %q, file: %q\n", dir, file)
// dir: "/home/user/", file: ""

SplitList

func SplitList(path string) []string

SplitList 使用操作系统特定的 ListSeparator 分割路径列表。

参数:

  • path - 路径列表字符串(如 PATH 环境变量)

返回值:

  • []string - 分割后的路径切片

说明:

  • 通常用于分割 PATH 或 GOPATH 环境变量
  • 与 strings.Split 不同,SplitList 在传入空字符串时返回空切片

示例:

// Unix
paths := filepath.SplitList("/usr/bin:/usr/local/bin:/home/user/bin")
fmt.Println(paths)
// [/usr/bin /usr/local/bin /home/user/bin]

// Windows
paths = filepath.SplitList("C:\\Windows;C:\\Windows\\System32")
fmt.Println(paths)
// [C:\Windows C:\Windows\System32]

// 空字符串
paths = filepath.SplitList("")
fmt.Println(paths)
// [] (空切片,不是包含空字符串的切片)

ToSlash

func ToSlash(path string) string

ToSlash 将 path 中的每个分隔符字符替换为斜杠字符。

参数:

  • path - 操作系统路径

返回值:

  • string - 斜杠分隔的路径

说明:

  • 多个分隔符被多个斜杠替换
  • 用于将操作系统路径转换为 io/fs 包使用的斜杠路径

示例:

path := `home\user\documents`  // Windows 路径
slashPath := filepath.ToSlash(path)
fmt.Println(slashPath)
// home/user/documents

使用场景:

  • 跨平台路径比较
  • 与 io/fs 包配合
  • URL 路径生成

VolumeName

func VolumeName(path string) string

VolumeName 返回前导卷标名。

参数:

  • path - 输入路径

返回值:

  • string - 卷标名(非 Windows 平台返回空字符串)

示例:

// Windows
fmt.Println(filepath.VolumeName("C:\\foo\\bar"))
// C:

fmt.Println(filepath.VolumeName("\\\\host\\share\\foo"))
// \\host\share

// Unix
fmt.Println(filepath.VolumeName("/home/user"))
// ""

Windows 卷标格式:

  • 驱动器号:C:
  • UNC 路径:\\server\share

Walk

func Walk(root string, fn WalkFunc) error

Walk 遍历以 root 为根的文件树,为树中的每个文件或目录调用 fn。

参数:

  • root - 根目录
  • fn - WalkFunc 类型的函数

返回值:

  • error - 错误(如果 fn 返回非 nil 且非 SkipDir/SkipAll)

说明:

  • 包括 root 本身
  • 文件按字典顺序遍历
  • Walk 在继续到下一个目录之前需要读取整个目录到内存
  • 不跟随符号链接
  • 所有错误由 fn 过滤

示例:

err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
    if err != nil {
        fmt.Printf("访问错误:%v\n", err)
        return err
    }
    
    fmt.Printf("访问:%s (%s)\n", path, info.Name())
    
    // 跳过特定目录
    if info.IsDir() && info.Name() == ".git" {
        return filepath.SkipDir
    }
    
    return nil
})

if err != nil {
    log.Fatal(err)
}

WalkFunc 行为:

  • 返回 nil - 继续遍历
  • 返回 SkipDir - 跳过当前目录
  • 返回 SkipAll - 跳过所有剩余(Go 1.20+)
  • 返回其他错误 - 停止遍历

错误处理:

// 情况 1: Lstat 失败
// fn 被调用,path 设为该路径,info 为 nil,err 为 Lstat 错误

// 情况 2: Readdirnames 失败
// fn 被调用,path 设为目录路径,info 为目录信息,err 为 Readdirnames 错误

WalkDir

func WalkDir(root string, fn fs.WalkDirFunc) error

WalkDir 遍历以 root 为根的文件树,为树中的每个文件或目录调用 fn。

参数:

  • root - 根目录
  • fn - fs.WalkDirFunc 类型的函数

返回值:

  • error - 错误

说明:

  • Go 1.16+ 引入
  • 与 Walk 类似,但使用 fs.DirEntry 而不是 os.FileInfo
  • 更高效:避免在每个访问的文件或目录上调用 os.Lstat
  • 按字典顺序遍历
  • 不跟随符号链接
  • 使用适合操作系统的分隔符

示例:

err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    if err != nil {
        fmt.Printf("访问错误:%v\n", err)
        return err
    }
    
    fmt.Printf("访问:%s (目录:%v)\n", path, d.IsDir())
    
    // 跳过特定目录
    if d.IsDir() && d.Name() == "vendor" {
        return filepath.SkipDir
    }
    
    return nil
})

if err != nil {
    log.Fatal(err)
}

Walk vs WalkDir:

// Walk - 使用 os.FileInfo(较慢)
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
    // info 已经通过 os.Lstat 获取
})

// WalkDir - 使用 fs.DirEntry(更快)
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    // d 是轻量级的,需要时再调用 d.Info()
})

五、典型示例

示例 1:查找特定扩展名的文件

package main

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

func findFilesByExt(root, ext string) ([]string, error) {
    var files []string
    
    err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        
        if !d.IsDir() && filepath.Ext(path) == ext {
            files = append(files, path)
        }
        
        return nil
    })
    
    return files, err
}

func main() {
    files, err := findFilesByExt(".", ".go")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    
    fmt.Printf("找到 %d 个 Go 文件:\n", len(files))
    for _, f := range files {
        fmt.Println(f)
    }
}

示例 2:计算目录大小

package main

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

func dirSize(path string) (int64, error) {
    var size int64
    
    err := filepath.WalkDir(path, func(fpath string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        
        if !d.IsDir() {
            info, err := d.Info()
            if err != nil {
                return err
            }
            size += info.Size()
        }
        
        return nil
    })
    
    return size, err
}

func main() {
    size, err := dirSize(".")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    
    fmt.Printf("目录大小:%d 字节\n", size)
    fmt.Printf("目录大小:%.2f MB\n", float64(size)/1024/1024)
}

示例 3:安全的文件路径处理

package main

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

func safeJoin(base, target string) (string, error) {
    // 清理目标路径
    cleanTarget := filepath.Clean(target)
    
    // 检查是否是本地路径
    if !filepath.IsLocal(cleanTarget) {
        return "", fmt.Errorf("路径不安全:%s", target)
    }
    
    // 连接路径
    fullPath := filepath.Join(base, cleanTarget)
    
    // 获取绝对路径
    absBase, err := filepath.Abs(base)
    if err != nil {
        return "", err
    }
    
    absFull, err := filepath.Abs(fullPath)
    if err != nil {
        return "", err
    }
    
    // 确保结果在 base 内
    if !strings.HasPrefix(absFull, absBase) {
        return "", fmt.Errorf("路径超出基础目录")
    }
    
    return absFull, nil
}

func main() {
    base := "/var/www/html"
    
    tests := []string{
        "images/logo.png",
        "../etc/passwd",
        "./style.css",
        "../../etc/shadow",
    }
    
    for _, t := range tests {
        path, err := safeJoin(base, t)
        if err != nil {
            fmt.Printf("✗ %s - %v\n", t, err)
        } else {
            fmt.Printf("✓ %s -> %s\n", t, path)
        }
    }
}

示例 4:批量重命名文件

package main

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

func batchRename(dir, oldExt, newExt string) error {
    return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        
        if d.IsDir() {
            return nil
        }
        
        if filepath.Ext(path) == oldExt {
            // 生成新文件名
            dir := filepath.Dir(path)
            base := strings.TrimSuffix(filepath.Base(path), oldExt)
            newPath := filepath.Join(dir, base+newExt)
            
            // 重命名
            if err := os.Rename(path, newPath); err != nil {
                return err
            }
            
            fmt.Printf("重命名:%s -> %s\n", path, newPath)
        }
        
        return nil
    })
}

func main() {
    err := batchRename("./docs", ".txt", ".md")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

示例 5:查找并删除临时文件

package main

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

func cleanTempFiles(root string) error {
    return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        
        if d.IsDir() {
            return nil
        }
        
        name := d.Name()
        if strings.HasPrefix(name, "temp_") || 
           strings.HasPrefix(name, ".tmp") ||
           strings.HasSuffix(name, ".tmp") {
            
            fmt.Printf("删除:%s\n", path)
            return os.Remove(path)
        }
        
        return nil
    })
}

func main() {
    err := cleanTempFiles(".")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("临时文件清理完成")
}

示例 6:构建跨平台路径

package main

import (
    "fmt"
    "path/filepath"
)

func buildConfigPath(appName, configFile string) string {
    // 获取用户家目录
    homeDir, _ := os.UserHomeDir()
    
    // 构建路径
    configDir := filepath.Join(homeDir, ".config", appName)
    configPath := filepath.Join(configDir, configFile)
    
    return configPath
}

func main() {
    path := buildConfigPath("myapp", "config.yaml")
    fmt.Println(path)
    // Unix: /home/user/.config/myapp/config.yaml
    // Windows: C:\Users\user\.config\myapp\config.yaml
}

示例 7:模式匹配文件

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    // 查找当前目录的所有 .go 文件
    matches, err := filepath.Glob("*.go")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Go 文件:%v\n", matches)
    
    // 查找子目录中的 .txt 文件
    matches, err = filepath.Glob("docs/*.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("文档文件:%v\n", matches)
    
    // 使用字符类
    matches, err = filepath.Glob("*.[ch]")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("C 源文件:%v\n", matches)
}

示例 8:解析符号链接

package main

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

func resolvePath(path string) (string, error) {
    // 转换为绝对路径
    absPath, err := filepath.Abs(path)
    if err != nil {
        return "", err
    }
    
    // 解析符号链接
    realPath, err := filepath.EvalSymlinks(absPath)
    if err != nil {
        return "", err
    }
    
    return realPath, nil
}

func main() {
    // 假设 /tmp/link -> /home/user
    path := "/tmp/link/file.txt"
    
    realPath, err := resolvePath(path)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    
    fmt.Printf("原始路径:%s\n", path)
    fmt.Printf("真实路径:%s\n", realPath)
}

示例 9:统计文件类型

package main

import (
    "fmt"
    "path/filepath"
)

func countFileTypes(root string) (map[string]int, error) {
    counts := make(map[string]int)
    
    err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        
        if d.IsDir() {
            return nil
        }
        
        ext := filepath.Ext(path)
        if ext == "" {
            ext = "(无扩展名)"
        }
        
        counts[ext]++
        return nil
    })
    
    return counts, err
}

func main() {
    counts, err := countFileTypes(".")
    if err != nil {
        fmt.Println(err)
        return
    }
    
    fmt.Println("文件类型统计:")
    for ext, count := range counts {
        fmt.Printf("  %s: %d\n", ext, count)
    }
}

示例 10:限制遍历深度

package main

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

func walkWithDepth(root string, maxDepth int) error {
    baseDepth := strings.Count(filepath.Clean(root), string(filepath.Separator))
    
    return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        
        // 计算当前深度
        currentDepth := strings.Count(filepath.Clean(path), string(filepath.Separator)) - baseDepth
        
        fmt.Printf("%s%s\n", strings.Repeat("  ", currentDepth), d.Name())
        
        // 如果达到最大深度,跳过子目录
        if currentDepth >= maxDepth && d.IsDir() {
            return filepath.SkipDir
        }
        
        return nil
    })
}

func main() {
    err := walkWithDepth(".", 2)
    if err != nil {
        fmt.Println(err)
    }
}

六、最佳实践

1. 使用 Join 而非字符串拼接

// ✓ 正确 - 使用 Join
fullPath := filepath.Join(baseDir, subDir, filename)

// ✗ 错误 - 字符串拼接
fullPath := baseDir + "/" + subDir + "/" + filename // 不跨平台

2. 使用 WalkDir 而非 Walk

// ✓ 推荐 - WalkDir 更高效(Go 1.16+)
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
    // 使用 d.IsDir() 等
})

// ✗ 不推荐 - Walk 较慢
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
    // info 已经调用过 Lstat
})

3. 安全的文件访问

// ✓ 正确 - 验证路径
func safePath(base, target string) (string, error) {
    clean := filepath.Clean(target)
    if !filepath.IsLocal(clean) {
        return "", fmt.Errorf("不安全的路径")
    }
    full := filepath.Join(base, clean)
    absBase, _ := filepath.Abs(base)
    absFull, _ := filepath.Abs(full)
    if !strings.HasPrefix(absFull, absBase) {
        return "", fmt.Errorf("路径超出基础目录")
    }
    return absFull, nil
}

4. 使用 Ext 检查文件类型

// ✓ 正确
if filepath.Ext(filename) == ".txt" {
    // 处理文本文件
}

// ✗ 错误 - 手动检查
if strings.HasSuffix(filename, ".txt") {
    // 可能错过 .TXT (Windows)
}

5. 使用 SplitList 处理 PATH

// ✓ 正确
paths := filepath.SplitList(os.Getenv("PATH"))

// ✗ 错误 - 使用 strings.Split
paths := strings.Split(os.Getenv("PATH"), ":") // Windows 不工作

6. 处理 Walk 错误

// ✓ 正确 - 处理错误
err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    if err != nil {
        fmt.Printf("访问错误:%v\n", err)
        return err
    }
    // 处理文件
    return nil
})
if err != nil {
    log.Fatal(err)
}

7. 使用 Clean 规范化路径

// ✓ 正确 - 总是清理用户输入
userPath := filepath.Clean(input)

// ✗ 错误 - 直接使用未清理的路径
userPath := input // 可能包含 .. 等

8. 使用 Rel 获取相对路径

// ✓ 正确
rel, err := filepath.Rel("/home/user", "/home/user/docs/file.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(rel) // docs/file.txt

七、与其他包配合

1. 与 os 包配合

import (
    "os"
    "path/filepath"
)

// 获取文件信息
info, err := os.Stat(filepath.Join(dir, filename))

// 创建文件
file, err := os.Create(filepath.Join(dir, filename))

2. 与 io/fs 包配合

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

// WalkDir 使用 fs.DirEntry
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    if d.IsDir() {
        // 处理目录
    }
    return nil
})

3. 与 strings 包配合

import (
    "path/filepath"
    "strings"
)

// 移除扩展名
name := strings.TrimSuffix(filename, filepath.Ext(filename))

// 检查前缀
if strings.HasPrefix(filepath.Base(path), "temp_") {
    // 临时文件
}

4. 与 path 包配合

import (
    "path"       // URL 路径
    "path/filepath" // 文件路径
)

// URL 路径使用 path
urlPath := path.Join("api", "v1")

// 文件路径使用 filepath
filePath := filepath.Join("docs", "readme.txt")

// 转换
osPath := filepath.FromSlash(urlPath)

八、快速参考

常量

常量说明UnixWindows
Separator路径分隔符/\
ListSeparator列表分隔符:;

变量

变量说明
ErrBadPattern模式格式错误
SkipDir跳过目录(WalkFunc 返回值)
SkipAll跳过所有剩余(Go 1.20+)

函数总览

函数说明
Abs返回绝对路径
Base返回文件名
Clean清理路径
Dir返回目录路径
EvalSymlinks解析符号链接
Ext返回扩展名
FromSlash斜杠转分隔符
Glob模式匹配文件
IsAbs检查绝对路径
IsLocal检查本地路径(Go 1.20+)
Join连接路径
Localize转换为操作系统路径(Go 1.20+)
Match模式匹配
Rel返回相对路径
Split分割为目录和文件
SplitList分割路径列表
ToSlash分隔符转斜杠
VolumeName返回卷标名(Windows)
Walk遍历文件树
WalkDir遍历文件树(Go 1.16+)

常用路径操作

操作函数示例
获取文件名BaseBase("/a/b.txt")"b.txt"
获取目录DirDir("/a/b.txt")"/a"
获取扩展名ExtExt("/a/b.txt")".txt"
清理路径CleanClean("/a/../b")"/b"
连接路径JoinJoin("a", "b")"a/b"
绝对路径AbsAbs("rel")"/abs/rel"
相对路径RelRel("/a", "/a/b")"b"
分割路径SplitSplit("/a/b")("/a/", "b")
解析链接EvalSymlinksEvalSymlinks("/link")"/real"

path vs filepath

特性pathpath/filepath
路径分隔符/ (总是)/ 或 \ (操作系统)
文件系统访问是(EvalSymlinks、Glob、Walk)
Windows 支持
用途URL 路径文件系统路径
跨平台依赖 OS

WalkFunc 返回值

返回值行为
nil继续遍历
SkipDir跳过当前目录
SkipAll跳过所有剩余(Go 1.20+)
其他错误停止遍历

九、注意事项

1. path 与 filepath 的选择

// path - URL 路径
urlPath := path.Join("api", "v1", "users") // "api/v1/users"

// filepath - 文件系统路径
filePath := filepath.Join("docs", "readme.txt")
// Unix: "docs/readme.txt"
// Windows: "docs\readme.txt"

2. Clean 不解析符号链接

// Clean 只做词法处理
clean := filepath.Clean("/link/../file")
// 结果:"/file" (即使 /link 是符号链接)

// 要解析符号链接,使用 EvalSymlinks
real := filepath.EvalSymlinks("/link/file")
// 访问文件系统,返回真实路径

3. Walk 不跟随符号链接

// Walk 跳过符号链接
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
    // 如果 path 是符号链接,不会进入
})

// 需要跟随符号链接,手动处理
info, _ := os.Lstat(path)
if info.Mode()&os.ModeSymlink != 0 {
    // 是符号链接
    realPath, _ := filepath.EvalSymlinks(path)
}

4. Glob 的模式限制

// Go 的 Glob 不支持 ** 递归匹配
matches, _ := filepath.Glob("**/*.go")
// 不会递归查找所有子目录

// 需要使用 WalkDir 实现递归查找

5. Windows 路径大小写

// Windows 路径不区分大小写
filepath.Equal("C:\\File.txt", "c:\\FILE.TXT") // true

// Unix 路径区分大小写
filepath.Equal("/File.txt", "/FILE.TXT") // false

6. Rel 的限制

// 无法计算跨驱动器的相对路径(Windows)
_, err := filepath.Rel("C:\\a", "D:\\b")
// 错误

// 无法处理非本地路径
_, err = filepath.Rel("/a", "./b")
// 错误:需要当前工作目录

7. IsLocal 是纯词法检查

// IsLocal 不考虑符号链接
filepath.IsLocal("file")      // true
filepath.IsLocal("../file")   // false
filepath.IsLocal("/abs")      // false

// 即使符号链接指向外部,也返回 true
// /link -> /etc/passwd
filepath.IsLocal("link")  // true (词法检查)

8. SplitList 空字符串处理

// SplitList 返回空切片(不是包含空字符串的切片)
paths := filepath.SplitList("")
fmt.Println(len(paths))  // 0

// strings.Split 返回包含空字符串的切片
paths2 := strings.Split("", ":")
fmt.Println(len(paths2)) // 1 ([""])

9. VolumeName 跨平台

// Unix 始终返回空
filepath.VolumeName("/home/user") // ""

// Windows 返回卷标
filepath.VolumeName("C:\\Windows")     // "C:"
filepath.VolumeName("\\\\server\\share") // "\\\\server\\share"

10. WalkDir 性能优势

// Walk - 每个文件调用 Lstat
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
    // info 已经获取
})

// WalkDir - 延迟获取 FileInfo
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
    // d 是轻量级的
    if needInfo {
        info, _ := d.Info() // 需要时才获取
    }
})

最后更新: 2026-04-05
Go 版本: Go 1.0+(WalkDir 为 Go 1.16+,IsLocal/Localize 为 Go 1.20+)
包文档: https://pkg.go.dev/path/filepath
相关包: path, os, io/fs, strings
跨平台指南: 文件路径处理最佳实践