版权声明:原创文章禁止转载
专栏目录:Golang 专栏(总目录)
Go 语言文件操作相关的包:os
、io/ioutil
(已过时)、path/filepath
1. 文件IO: os
对文件的 I/O 相关的操作,封装在系统相关的包 os
里面。
1.1 打开文件: os.OpenFile()
打开一个文件的通用函数: os.OpenFile()
os.OpenFile(name string, flag int, perm FileMode) (*File, error)
// 参数说明:
// name 文件路径
// flag 文件的打开方式
// perm 文件的权限位, FileMode 的底层类型是 uint32
//
// 返回文件对象和错误 (*os.File, error)
// flag 的取值 (并不是所有的标志都可以在给定的系统上实现):
//
// os.O_RDONLY // 只读
// os.O_WRONLY // 只写
// os.O_RDWR // 读写
//
// 以上三个值必须指定其中一个, 剩余的值可以使用 或| 运算添加控制
//
// os.O_APPEND // 在写入时将数据附加到文件中
// os.O_CREATE // 如果不能存在则创建新文件
// os.O_EXCL // 与 O_CREATE 一起使用, 文件不能已存在, 存在则返回错误
// os.O_SYNC // 以同步 I/O 的方式打开
// os.O_TRUNC // 打开时截断常规可写文件
// perm 用于表示文件权限为, 需要创建文件时才使用, FileMode 的底层类型是 uint32,
// 一般使用 3 位 8 进制整数表示, 例如:
//
// 0777 // 相当于 rwxrwxrwx, 表示所有用户都可以读写和运行
// 0666 // 相当于 rw-rw-rw-, 表示所有用户都可以读写
一般情况下不直接使用 os.OpenFile()
函数打开文件,而是使用另外两个更加简洁的函数 os.Open()
和 os.Create()
:
// 用于读取文件, 以只读的方式打开, 用于读取文件,
// 相当于: os.OpenFile(name, O_RDONLY, 0)
os.Open(name string) (*File, error)
// 用于写入文件, 以可读写的方式打开, 如果文件不存在则创建, 如果文件存在则截断文件内容(相当于删除重建)
// 相当于: os.OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
os.Create(name string) (*File, error)
如果需要添加内容到文件末尾,需使用 os.OpenFile()
函数指定 os.O_APPEND
标志:
// 以可读写的方式打开文件, 如果文件不存在则创建, 写入内容时添加到文件尾 (不能使用截断)
os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
打开文件函数返回两个参数 (*File, error)
分别表示 文件对象 和 错误值,当有返回错误时(err != nil
),使用 os.IsXXX()
可以判断错误类型:
// 打开文件成功返回 文件对象(*File) 和 nil, 打开失败返回 nil 和 错误值
f, err := os.OpenFile(...)
if err != nil {
if os.IsExist(err) {
// 文件已存在错误: 用于 写入文件但不创建文件(os.O_EXCL) 的时候
fmt.Println("文件已存在:", err)
} else if os.IsNotExist(err) {
// 文件不存在错误: 用于 只读打开 的时候
fmt.Println("文件不存在:", err)
} else if os.IsPermission(err) {
// 没有访问权限错误
fmt.Println("没有权限:", err)
} else {
// 其他错误
fmt.Println("其他错误:", err)
}
return
}
...
_ = f.Close()
1.2 文件对象的方法: *os.File
文件对象 f *os.File
的常用方法:
// 读取方法
f.Read(b []byte) (n int, err error) // 读取到切片
// 写入方法
f.Write(b []byte) (n int, err error) // 写入字节序列
f.WriteString(s string) (n int, err error) // 写入字符串
f.ReadFrom(r io.Reader) (n int64, err error) // 从读取器中读取数据写入文件
// 随机读写
f.ReadAt(b []byte, off int64) (n int, err error) // 从文件头部偏移指定位置开始读取
f.WriteAt(b []byte, off int64) (n int, err error) // 从文件头部偏移指定位置开始写入
f.Seek(offset int64, whence int) (ret int64, err error) // 偏移读写指针的位置, whence 表示偏移的相对位置,
// whence 的取值: io.SeekStart, io.SeekCurrent, io.SeekEnd
// 文件夹操作 (目录也支持打开, os.File 也可以表示一个目录)
f.Chdir() error // 改变当前工作目录为 f 表示的目录
f.Readdir(n int) ([]FileInfo, error) // 读取目录下的所有文件和文件夹, 以 []FileInfo 方式返回
f.ReadDir(n int) ([]DirEntry, error) // 读取目录下的所有文件和文件夹, 以 []DirEntry 方式返回
f.Readdirnames(n int) (names []string, err error) // 读取目录下的所有文件和文件夹, 以文件名称切片的方式返回
// 这三个 读取文件夹 的方法均为分批读取, 一次读取最多返回 n 个,
// 下次继续读取, 直到全部读取完毕后返回 io.EOF 的错误。
// n <= 0 表示一次性读取全部。
// 其他方法
f.Name() string // 文件路径, 打开时传递的 name 参数
f.Stat() (FileInfo, error) // 文件信息 (Name, Size, Mode, ModTime, IsDir)
f.Close() error // 关闭文件, 文件成功打开后, 不在需要使用时必须关闭
f.Chmod(mode FileMode) error // 改变文件的权限位
f.Chown(uid, gid int) error // 改变文件的拥有者
1.3 读取文件: os.Open()
os.Open(name)
函数内部调用了 os.OpenFile(name, O_RDONLY, 0)
函数。读取文件示例:
package main
import (
"bytes"
"fmt"
"io"
"os"
)
func main() {
// 打开文件 (只读方式)
f, err := os.Open("demo.txt")
if err != nil {
if os.IsNotExist(err) {
fmt.Println("文件不存在:", err)
} else {
fmt.Println("打开文件出错:", err)
}
return
}
defer func(f *os.File) {
_ = f.Close()
}(f)
// 创建一个字节缓冲区, 用于保存分块读取的文件内容
buf := bytes.NewBuffer(nil)
// 创建字节切片, 用于分块读取
bs := make([]byte, 1024)
for {
// 从文件中读取字节数, 保存到 切片bs 中,
// 返回本次读取的 字节数 和 错误值, 最多读取 len(bs) 个字节
n, err := f.Read(bs)
if err != nil {
if err == io.EOF {
// 到达文件尾部, 如果同时读取了数据, 也需要一起写入缓冲区
if n > 0 {
buf.Write(bs[:n])
}
} else {
// 其他读取错误
fmt.Println("读取出错:", err)
}
// 到达文件尾部 或 读取出错, 不再继续
break
}
// 本次从文件读取到的字节数写入缓冲区
if n > 0 {
buf.Write(bs[:n])
}
}
// 获取读取到的字节序列
fileContent := buf.Bytes()
// 把 字节序列 转换为 字符串 输出
fmt.Println(string(fileContent))
// Go 的字符串使用 UTF-8 编码, 文件编码格式必须是 UTF-8 编码,
// 其他编号需手动转换为 UTF-8 编码后才能转换为 Go 字符串输出。
}
以上方式是自己编写分区块读取的细节。可以使用下面两个封装好的函数快速读取文件:
os.ReadFile(name string) ([]byte, error) // 读取文件, 返回 整个文件的字节序列 和 错误值
ioutil.ReadFile(name string) ([]byte, error) // 内部调用 os.ReadFile(name)
1.4 写入文件: os.Create()
os.Create(name)
函数内部调用了 os.OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
函数。写入文件示例:
package main
import (
"fmt"
"os"
)
func main() {
// 打开文件 (可读写方式, 文件不存在则创建, 文件存在则截断)
f, err := os.Create("demo.txt")
if err != nil {
fmt.Println("打开文件错误:", err)
return
}
defer func(f *os.File) {
_ = f.Close()
}(f)
// 写入 字节序列
_, _ = f.Write([]byte("Hello World"))
// 写入字符串 (Go 字符串使用 UTF-8 编码, 写入的实际是 UTF-8 编码的字节序列)
_, _ = f.WriteString("你好 世界")
// 从一个文件中读取内容写入文件
fr, err := os.Open("abc.txt")
if err != nil {
fmt.Println("打开文件错误:", err)
return
}
defer func(fr *os.File) {
_ = fr.Close()
}(fr)
// 从 文件fr 读取内容写入到 文件f
_, _ = f.ReadFrom(fr)
}
可以使用下面两个函数快速写数据到文件:
os.WriteFile(name string, data []byte, perm FileMode) error // 把 字节序列 写入到 文件
ioutil.WriteFile(filename string, data []byte, perm fs.FileMode) error // 内部调用 os.WriteFile(...)
1.5 同时读写 和 追加内容
同时读写 和 追加内容 示例:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.OpenFile("demo.txt", os.O_RDWR|os.O_APPEND, 0)
if err != nil {
fmt.Println("打开文件出错:", err)
return
}
defer func(f *os.File) {
_ = f.Close()
}(f)
bs := make([]byte, 10)
// 读取时默认从头部开始读取
n, err := f.Read(bs)
fmt.Printf("读取的内容: %s, err: %v\n", string(bs[:n]), err)
// 添加了 os.O_APPEND 方式, 写入时默认写到尾部
n, err = f.WriteString("abc123") // 写入数据,
fmt.Printf("写入的字节数: %d, err: %v\n", n, err)
}
1.6 快速读写文件: os.ReadFile()
和 os.WriteFile()
os
包中的快速读写函数:
os.ReadFile(name string) ([]byte, error)
os.WriteFile(name string, data []byte, perm FileMode) error
快速读写文件示例:
package main
import (
"fmt"
"os"
)
func main() {
// 写入文件, 内部使用 os.OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm) 的方式打开文件
err := os.WriteFile("demo.txt", []byte("Hello World"), 0666)
if err != nil {
fmt.Println("写入文件出错:", err)
}
// 读取文件, 直接返回整个文件的字节序列, 内部使用 os.OpenFile(name, O_RDONLY, 0) 的方式打开文件
bs, err := os.ReadFile("demo.txt")
if err != nil {
fmt.Println("读取文件出错:", err)
} else {
fmt.Println("读取到的文件内容:", string(bs))
}
}
1.7 判断路径 是否存在、是否是文件/目录/符号链接
通过 os.Stat()
获取文件的信息来判断文件是 否存在 和 是否是文件/目录/符号链接:
package main
import (
"fmt"
"os"
)
func main() {
file := "demo.txt"
fmt.Println(IsExist(file)) // 是否存在
fmt.Println(IsFile(file)) // 是否是文件
fmt.Println(IsDir(file)) // 是否是目录
fmt.Println(IsSymlink(file)) // 是否是符号链接
}
// IsExist 路径是否存在
func IsExist(name string) bool {
_, err := os.Stat(name)
return !os.IsNotExist(err)
}
// IsFile 是否是文件
func IsFile(name string) bool {
fi, err := os.Stat(name)
return err == nil && !fi.IsDir()
}
// IsDir 是否是文件夹
func IsDir(name string) bool {
fi, err := os.Stat(name)
return err == nil && fi.IsDir()
}
// IsSymlink 是否是符号链接
func IsSymlink(name string) bool {
fi, err := os.Lstat(name)
return err == nil && fi.Mode()&os.ModeSymlink != 0
}
os.Stat()
与 os.Lstat()
的区别:如果路径是一个符号链接,Stat() 会尝试跟踪链接返回原路径的属性,Lstat() 则不会跟踪链接符号直接返回符号链接的属性。
1.8 os
包中的其他文件相关操作
os.PathSeparator // rune类型的字符, 当前系统的路径中的分隔符, MacOS/Linux 系统中为 "/"
os.PathListSeparator // rune类型的字符, 当前系统的多个路径的分隔符, MacOS/Linux 系统中为 ":"
os.Mkdir(name string, perm FileMode) error // 创建文件夹
os.MkdirAll(path string, perm FileMode) error // 创建多级文件夹
os.MkdirTemp(dir, pattern string) (string, error) // 创建临时文件夹, 一般结合 defer os.RemoveAll(dir) 使用
os.CreateTemp(dir, pattern string) (*File, error) // 创建临时文件, 一般结合 defer os.Remove(f.Name()) 使用
os.ReadDir(name string) ([]DirEntry, error) // 读取目录下的所有文件和文件夹
os.Lstat(name string) (FileInfo, error) // 读取路径的文件信息, 如果路径是符号链接, 则不会尝试跟踪该链接 (返回符号链接)
os.Stat(name string) (FileInfo, error) // 读取路径的文件信息, 如果路径是符号链接, 则尝试跟踪该链接 (返回原路径的属性)
os.Remove(name string) error // 删除文件或目录 (如果是目录, 目录下不能有文件/文件夹)
os.RemoveAll(path string) error // 删除整个目录 (必须谨慎使用)
os.Rename(oldpath, newpath string) error // 重命名文件/目录
os.Chmod(name string, mode FileMode) error // 改变文件/目录的权限位
os.Chown(name string, uid, gid int) error // 改变文件/目录的拥有者
os.Symlink(oldname, newname string) error // 创建 符号链接
os.Readlink(name string) (string, error) // 获取 符号链接 的 原路径
os.Expand(s string, mapping func(string) string) string // 展开路径, 把类似 $PATH 的占位符调动 mapping 函数替换为字符串
os.ExpandEnv(s string) string // 展开路径, 根据环境变量映射, 相当于 os.Expand(s, os.Getenv)
os.Getwd() (dir string, err error) // 获取当前工作目录
os.Chdir(dir string) error // 改变当前工作目录
os.UserHomeDir() (string, error) // 用户主目录
os.UserCacheDir() (string, error) // 用户缓存目录
os.UserConfigDir() (string, error) // 用户配置目录
os.TempDir() string // 临时目录
os.SameFile(fi1, fi2 FileInfo) bool // 判断两个文件是否为同一个文件
2. IO工具: io/ioutil
io/ioutil
模块封装了许多方便的 IO 操作:
// 读取文件, 内部调用 os.ReadFile(...)
ioutil.ReadFile(filename string) ([]byte, error)
// 写入文件, 内部调用 os.WriteFile(...)
ioutil.WriteFile(filename string, data []byte, perm fs.FileMode) error
// 从一个读取器中读取所有的内容
ioutil.ReadAll(r io.Reader) ([]byte, error)
// 读取目录, 返回目录下的 所有文件和目录, 内部调用 f.Readdir(-1)
ioutil.ReadDir(dirname string) ([]fs.FileInfo, error)
// 创建一个临时文件, os.CreateTemp(...), 搭配 defer os.Remove(f.Name()) 使用 (用完删除)
ioutil.TempFile(dir, pattern string) (f *os.File, err error)
// 创建一个临时目录, os.MkdirTemp(...), 搭配 defer os.RemoveAll(dir) 使用 (用完删除)
ioutil.TempDir(dir, pattern string) (name string, err error)
// 返回一个 io.ReadCloser 接口示例, 其中包含一个包装 io.Reader r, 并且提供了一个 Colos() 方法 (方法内无任何操作)
ioutil.NopCloser(r io.Reader) io.ReadCloser
io/ioutil
包中的函数在 Go 1.16 后已过时,可以直接使用os
包内的相关函数。
3. 文件/路径属性: os.FileInfo
os.FileInfo
是一个接口,表示一个文件或路径的属性信息,接口中的方法:
Name() string // 文件/目录的路径名称
Size() int64 // 文件大小
Mode() FileMode // 文件的模式位, 底层类类型为 uint32, 保存了权限信息、目录/符号链接标志等信息
ModTime() time.Time // 修改时间
IsDir() bool // 是否是文件夹, 内部调用 Mode().IsDir()
Sys() any // 基础的数据源 (可以返回 nil)
获取文件属性使用 os.Stat()
和 os.Lstat()
函数。如果路径是一个符号链接,Stat() 会尝试跟踪链接返回原路径的属性,Lstat() 则不会跟踪链接符号直接返回符号链接的属性。
获取文件/路径属性示例:
package main
import (
"fmt"
"os"
)
func main() {
fi, err := os.Lstat("main.go")
if err != nil {
fmt.Println("获取文件/目录属性出错:", err)
return
}
fmt.Println(fi.Name())
fmt.Println(fi.Size())
fmt.Println(fi.ModTime())
fmt.Println(fi.IsDir())
fmt.Println(fi.Mode()&os.ModeSymlink != 0)
fmt.Println(fi.IsDir())
}
4. 遍历文件夹: os.ReadDir()
读取文件夹下的所有文件和文件夹,有以下几个函数:
var f *os.File = ...
f.Readdir(n int) ([]FileInfo, error) // 以 []FileInfo 方式返回
f.ReadDir(n int) ([]DirEntry, error) // 以 []DirEntry 方式返回
f.Readdirnames(n int) (names []string, err error) // 只返回 文件/文件夹 的名称
os.ReadDir(name string) ([]DirEntry, error) // 内部调用 f.Readdir(-1)
ioutil.ReadDir(dirname string) ([]fs.FileInfo, error) // 内部调用 f.ReadDir(-1)
// 以上所有函数, 实际上最终都是调用文件对象的一个私有方法 f.readdir(...)
以 []FileInfo
方式遍历文件夹示例:
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
fis, err := ioutil.ReadDir(".")
if err != nil {
fmt.Println("读取文件夹出错:", err)
return
}
for _, fileInfo := range fis {
fmt.Println(fileInfo.Name()) // 文件名
fmt.Println(fileInfo.IsDir()) // 是否是文件夹
fmt.Println(fileInfo.Mode()&os.ModeSymlink != 0) // 是否是符号链接
fmt.Println(fileInfo.Size()) // 文件大小
fmt.Println(fileInfo.ModTime()) // 修改时间
fmt.Println()
}
}
以 []DirEntry
方式遍历文件夹示例:
package main
import (
"fmt"
"os"
)
func main() {
entries, err := os.ReadDir(".")
if err != nil {
fmt.Println("读取文件夹出错:", err)
return
}
for _, dirEntry := range entries {
fmt.Println(dirEntry.Name()) // 文件名
fmt.Println(dirEntry.IsDir()) // 是否是文件夹
fmt.Println(dirEntry.Type()&os.ModeSymlink != 0) // 是否是符号链接
fileInfo, err := dirEntry.Info() // 获取文件信息(FileInfo)
if err != nil {
fmt.Println("获取文件信息出错:", err)
continue
}
fmt.Println(fileInfo.Size()) // 文件大小
fmt.Println(fileInfo.ModTime()) // 修改时间
fmt.Println()
}
}
5. 文件路径操作: path/filepath
path/filepath
包封装了对 文件路径 的操作。相关操作以与目标系统定义的文件路径相兼容的方式操作文件名路径。
path/filepath
包中的函数:
filepath.Separator = os.PathSeparator // 当前系统的路径中的分隔符, macOS/Linux 系统中为 "/"
filepath.ListSeparator = os.PathListSeparator // 当前系统的多个路径的分隔符, macOS/Linux 系统中为 ":"
filepath.Abs(path string) (string, error) // 返回路径的绝对路径
filepath.IsAbs(path string) bool // 判断路径是否是绝对路径
filepath.Join(elem ...string) string // 拼接多个路径, 支持 ..
filepath.Base(path string) string // 返回路径所表示的 文件/文件夹名称
filepath.Dir(path string) string // 返回路径的父目录 (路径所在的目录)
filepath.Split(path string) (dir, file string) // 返回路径的 父目录 和 文件/文件夹名称
filepath.Ext(path string) string // 返回路径的扩展名 (包括点)
filepath.Rel(basepath, targpath string) (string, error) // 返回相对路径, targpath 相对于 basepath 的路径
filepath.Clean(path string) string // 清理链接, 返回与 path 等效的最短路径名, 例如: "/a/b/../c//" -> "/a/c"
filepath.EvalSymlinks(path string) (string, error) // 返回符号链接 path 的真实路径, 结果会调用 Clean() 清理路径
filepath.VolumeName(path string) string // Windows 系统返回盘符名(如 "C:"), 其他系统返回 ""
filepath.SplitList(path string) []string // 使用当前系统的多路径分隔符(filepath.ListSeparator)拆分路径 (如 ":" 或 ";")
filepath.ToSlash(path string) string // 把路径中的 当前系统平台分隔符(filepath.Separator) 替换为 "/"
filepath.FromSlash(path string) string // 把路径中的 "/" 替换为 当前系统平台分隔符(filepath.Separator)
filepath.Glob(pattern string) (matches []string, err error) // 找出匹配 pattern 路径的所有文件或目录, 如: Glob("/a/*.txt")、Glob("./h?.txt")
filepath.Match(pattern, name string) (matched bool, err error) // 判断 路径name 是是否匹配 pattern, 如: Match("*.txt", "aa.txt")
filepath.Walk(root string, fn WalkFunc) error // 遍历以 root 为根的文件树, 为树中的每一个文件或目录回到 fn 函数, 包括 root 根路径
filepath.WalkDir(root string, fn fs.WalkDirFunc) error // 与 Walk() 功能一样, 只不过 Walk() 用 FileInfo 回调, WalkDir() 用 DirEntry 回调
6. 使用 filepath.Walk()
遍历文件夹
使用 filepath.Walk()
或 filepath.WalkDir()
可以很方便的深度遍历整个目录树:
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
// Walk() 函数会回调遍历到的每一个 文件/目录 (包括子目录 和 根目录自己)
fn := func(path string, info os.FileInfo, err error) error {
fmt.Printf("%s, %s, %v, %v\n", path, info.Name(), info.Mode(), info.ModTime())
return nil
}
// 遍历当前文件夹
_ = filepath.Walk(".", fn)
}