Go对文件操作的支持很健壮.
下面的程序打开,读取并关闭了一个文件:
path := "main.go"
f, err := os.Open(path)
if err != nil {
log.Fatalf("os.Open() failed with %s\n", err)
}
defer f.Close()
d, err := ioutil.ReadAll(f)
if err != nil {
log.Fatalf("ioutil.ReadAll() failed with '%s'\n", err)
}
lines := bytes.Split(d, []byte{'\n'})
fmt.Printf("File %s has %d lines\n", path, len(lines))
File main.go has 29 lines
读文件
Read the whole fil读取全部文件
读取整个文件最简单的方法:
d, err := ioutil.ReadFile("foo.txt")
if err != nil {
log.Fatalf("ioutil.ReadFile failed with '%s'\n", err)
}
fmt.Printf("Size of 'foo.txt': %d bytes\n", len(d))
Open file for reading, close file
f, err := os.Open("foo.txt")
if err != nil {
log.Fatalf("os.Open failed with '%s'\n", err)
}
defer f.Close()
Open返回* os.File,它实现io.Reader和io.Closer接口。
您应该始终关闭文件,以避免泄漏文件描述符。 defer非常适合确保在函数退出时调用Close。
逐行读取文件
func ReadLines(filePath string) ([]string, error) {
file, err := os.OpenFile(filePath, os.O_RDONLY, 0666)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
res := make([]string, 0)
for scanner.Scan() {
line := scanner.Text()
res = append(res, line)
}
if err = scanner.Err(); err != nil {
return nil, err
}
return res, nil
}
func main() {
path := "main.go"
lines, err := ReadLines(path)
if err != nil {
log.Fatalf("ReadLines failed with '%s'\n", err)
}
fmt.Printf("File %s has %d lines\n", path, len(lines))
}
File main.go has 41 lines
写文件
写文件最简单的方式:
d := []byte("content of the file")
err := ioutil.WriteFile("foo.txt", d, 0644)
if err != nil {
log.Fatalf("ioutil.WriteFile failed with '%s'\n", err)
}
Open file for writing
f, err := os.Create("foo.txt")
if err != nil {
log.Fatalf("os.Open failed with '%s'\n", err)
}
Create返回* os.File,它实现io.Writer和io.Closer接口。
如果文件不存在,则会创建该文件。
如果文件确实存在,它将被截断。
您应该始终关闭文件,以避免泄漏文件描述符。
请注意,关闭文件可能会返回错误,因此对于强大的代码,您应该检查关闭是否有错误。
可以缓冲写操作,Close可能需要将剩余的缓存字节刷新到磁盘。 那可能会失败并返回错误。
打开文件进行追加
f, err := os.OpenFile(filePath, os.O_WRONLY | os.O_APPEND | os.O_CREATE, 0666)
if err != nil {
log.Fatalf("os.Open failed with '%s'\n", err)
}
os.OpenFile的第二个参数是一个标志,用于确定打开文件的确切模式。
打开读取时,请使用os.Open。
当您打开以写入新文件时,请使用os.Create。
当打开以追加到现有文件时,请使用带有以下标志的os.OpenFile:* os.O_WRONLY。 如果我们也要读写,也可以是os.RDWR。* os.O_APPEND表示如果文件存在,我们将附加* os.O_CREATE表示如果文件不存在,我们将创建它。 如果没有此标志,打开不存在的文件将失败
写文件
d := []byte("data to write")
nWritten, err := f.Write(d)
创建文件时的文件权限
使用os.Create或os.OpenFile创建新文件时,需要为新文件提供文件权限。
在大多数情况下,0644是一个不错的选择。
这些是八进制格式的Unix风格权限。
让我们解构0644
- 0:表示这是八进制格式的数字。 这意味着每个数字都在0-7范围内(与每个数字都在0-9范围内的常规十进制表示法比较)
- 6:文件创建者的权限。 这是位掩码4 +2。4表示读取权限,2表示写入权限。 结合使用时,意味着读/写访问
- 4:用户所属组的权限。 即 也属于同一组的每个用户都将具有这些权限。 4表示仅读取权限
- 4:所有用户的权限。 同样,4表示仅读取权限
为了完整起见,1代表可执行权限,因此如果这是一个可执行文件,则权限标志将为0755,即对于每个用户/组/匿名组件,我们将1设置为可执行位
这些权限是Unix和Mac OS固有的,但是在Windows上它们的映射很松散。
要管理Windows权限,您需要使用Windows API。
File操作
获取文件大小:
// GetFileSize returns file size or error if e.g. file doesn't exist
func GetFileSize(path string) (int64, error) {
st, err := os.Lstat(path)
if err != nil {
return -1, err
}
return st.Size(), nil
}
func main() {
path := "main.go"
size, err := GetFileSize(path)
if err != nil {
log.Fatalf("GetFileSize failed with '%s'\n", err)
}
fmt.Printf("File %s is %d bytes in size\n", path, size)
}
ile main.go is 501 bytes in size
除了os.Stat,我们还可以使用os.Lstat。 区别在于os.Stat遵循符号链接,而os.Lstat不遵循符号链接。
换句话说:对于符号链接,os.Lstat返回有关链接的信息,os.Stat返回有关链接到的文件的信息。
获取文件信息:
func main() {
st, err := os.Stat("main.go")
if err != nil {
log.Fatalf("GetFileSize failed with '%s'\n", err)
}
fmt.Printf(`Name: %s
Size: %d
IsDir: %v
Mode: %x
ModTime: %s
OS info: %#v
`, st.Name(), st.Size(), st.IsDir(), st.Mode, st.ModTime(), st.Sys())
}
Name: main.go
Size: 363
IsDir: false
Mode: 4999c0
ModTime: 2019-11-06 04:41:24.675411079 +0000 UTC
OS info: &syscall.Stat_t{Dev:0x5, Ino:0xba8, Nlink:0x1, Mode:0x81a4, Uid:0x0, Gid:0x0, X__pad0:0, Rdev:0x0, Size:363, Blksize:4096, Blocks:8, Atim:syscall.> Timespec{Sec:1573015285, Nsec:208585110}, Mtim:syscall.Timespec{Sec:1573015284, Nsec:675411079}, Ctim:syscall.Timespec{Sec:1573015284, Nsec:675411079}, > X__unused:[3]int64{0, 0, 0}}
检查文件是否存在:
// IsPathxists returns true if a given path exists, false if it doesn't.
// It might return an error if e.g. file exists but you don't have
// access
func IsPathExists(path string) (bool, error) {
_, err := os.Lstat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
// error other than not existing e.g. permission denied
return false, err
}
func printExists(path string) {
exists, err := IsPathExists(path)
if err == nil {
fmt.Printf("File '%s' exists: %v\n", path, exists)
} else {
fmt.Printf("IsFileExists('%s') failed with '%s'\n", path, err)
}
}
func main() {
printExists("main.go")
printExists("non-existent-file.txt")
}
File 'main.go' exists: true
File 'non-existent-file.txt' exists: false
令人惊讶的是,检查文件是否存在是很难的,并且不可能编写处理所有细微差别的通用函数。
这是我们做出的决定:
它对待文件和目录相同。 如果存在路径,并且要区分目录和文件,则需要在os.Lstat的结果上调用IsDir()
如果文件是符号链接,我们是否测试链接或链接到的实际文件? 我们使用了os.Lstat,所以我们测试了链接。 我们也可以使用os.Stat来解析符号链接
“路径不存在”只是os.Lstat返回的可能错误之一。 我们要区分“文件不存在”和“文件存在并且我们无权访问”吗? 我们决定提供更多信息,但在某些情况下,仅返回bool并在os.Lstat失败时始终返回false会更简单。
删除文件
path := "foo.txt"
err := os.Remove(path)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("os.Remove failed because file doesn't exist\n")
} else {
fmt.Printf("os.Remove failed with '%s'\n", err)
}
}
os.Remove对于不存在的文件返回错误。
通常,您想忽略此类错误,可以通过使用os.IsNotExist(err)测试错误来完成。
重命名文件
oldPath := "old_name.txt"
newPath := "new_name.txt"
err := os.Rename(oldPath, newPath)
if err != nil {
fmt.Printf("os.Rename failed with '%s'\n", err)
}
拷贝文件
// CopyFile copies a src file to dst
func CopyFile(dst, src string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return err
}
_, err = io.Copy(dstFile, srcFile)
err2 := dstFile.Close()
if err == nil && err2 != nil {
err = err2
}
if err != nil {
// delete the destination if copy failed
os.Remove(dst)
}
return err
}
编写用于复制文件的通用函数非常棘手,并且不可能编写可满足所有用例的函数。
以下是我们制定的政策决策:
如果目标存在,我们应该覆盖现有文件还是返回错误? 我们决定改写
新文件应具有什么权限? 我们决定了使用默认权限的最简单情况。 另一种选择是从源复制权限或允许调用方提供权限
如果您想要不同的行为,则必须根据需要修改代码。
目录操作
创建目录
dir := "my_dir"
err := os.Mkdir(dir, 0755)
if err != nil {
fmt.Printf("os.Mkdir('%s') failed with '%s'\n", dir)
}
dir := filepath.Join("topdir", "subdir")
err := os.MkdirAll(dir, 0755)
if err != nil {
fmt.Printf("os.MkdirAll('%s') failed with '%s'\n", dir)
}
os.Mkdir仅在dir的父目录已存在时才成功。
os.MkdirAll将创建所有中间目录。
0755描述了目录的权限。
这些是八进制格式的Unix样式权限。
让我们解构0755
- 0的部分表示这是八进制格式的数字。 这意味着每个数字都在0-7范围内(与每个数字在0-9范围内的常规十进制表示法相比)
- 7是文件创建者的权限。 这是位掩码4 + 2 +1。4表示读取权限,2表示写入权限,1表示可以遍历目录。 组合使用时,意味着读/写/遍历访问
- 5是用户所属组的权限。 即 也属于同一组的每个用户都将具有这些权限。 4 + 1表示读取和遍历权限,但不具有写入权限
- 5所有人权限。 同样,5表示读取和遍历权限
删除目录
dir := "my_dir"
err := os.Remove(dir)
if err != nil {
fmt.Printf("os.Remove('%s') failed with '%s'\n", path, err)
}
os.Remove仅适用于空目录,即没有任何子目录和文件的目录。
dir := "my_dir"
err := os.RemoveAll(dir)
if err != nil {
fmt.Printf("os.RemoveAll('%s') failed with '%s'\n", path, err)
}
os.RemoveAll删除目录及其所有子目录(文件和子目录)。
列出目录中的文件
使用ioutil.ReadDir列出给定目录下的文件:
func main() {
dir := "."
fileInfos, err := ioutil.ReadDir(dir)
if err != nil {
log.Fatalf("ioutil.ReadDir('%s') failed with '%s'\n", dir, err)
}
for i, fi := range fileInfos {
if i < 4 {
fmt.Printf("Path: %s, is dir: %v, size: %d bytes\n", fi.Name(), fi.IsDir(), fi.Size())
}
}
}
Path: app, is dir: false, size: 2202180 bytes
Path: go.mod, is dir: false, size: 21 bytes
Path: main.go, is dir: false, size: 400 bytes
递归列出文件
func main() {
nShown := 0
err := filepath.Walk(".", func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if nShown > 4 {
return nil
}
nShown++
fmt.Printf("Path: %s, is dir: %v, size: %d bytes\n", fi.Name(), fi.IsDir(), fi.Size())
return nil
})
if err != nil {
fmt.Printf("filepath.Walk failed with '%s'\n", err)
}
}
Path: ., is dir: true, size: 0 bytes
Path: app, is dir: false, size: 2179294 bytes
Path: go.mod, is dir: false, size: 21 bytes
Path: main.go, is dir: false, size: 466 bytes
要递归访问目录中的文件,请使用filepath.Walk。
提供了一个回调函数,该函数将为目录下的每个文件和目录调用。
甚至对于我们无法读取的文件/目录(如权限不足),也会调用回调函数。
我们可以通过从回调函数返回非nil错误来尽早结束遍历。
文件目录操作
不幸的是,不同的操作系统对文件路径的格式具有不同的规则。
例如,在Unix和Mac OS上,路径分隔符为/,而在Windows上为\。
对于可移植程序,重要的是使用文件路径包中的函数,这些函数应了解给定操作系统使用的约定。
注意:filepath软件包管理OS文件路径。 也有具有类似功能的路径包,但它始终使用/作为路径分隔符。
合并路径
path := filepath.Join("dir", "sub", "file.txt")
fmt.Printf("path: %s\n", path)
path: dir/sub/file.txt
您可以加入两个以上的路径元素。
在Windows上,以上将返回dir \ file.txt,在Unix和Mac OS上,将返回dir / file.txt。
将路径拆分为目录和文件
path := filepath.Join("dir", "file.txt")
file := filepath.Base(path)
fmt.Printf("path: %s, file: %s\n", path, file)
path: dir/file.txt, file: file.txt
分割路径list
parts := filepath.SplitList("/usr/bin:/tmp")
fmt.Printf("parts: %#v\n", parts)
parts: []string{"/usr/bin", "/tmp"}
从path获取文件名
path := filepath.Join("dir", "file.txt")
dir, file := filepath.Split(path)
fmt.Printf("dir: %s, file: %s\n", dir, file)
dir: dir/, file: file.txt
从path获取路径名
path := filepath.Join("dir", "file.txt")
dir := filepath.Dir(path)
fmt.Printf("path: %s, dif: %s\n", path, dir)
path: dir/file.txt, dif: dir
获取文件后缀
ext := filepath.Ext("file.txt")
fmt.Printf("ext: %s\n", ext)
ext: .txt
I/O相关接口
Go标准库定义了几个与I / O相关的接口。
它们对于从具体对象中抽象I / O操作至关重要。
借助io.Reader接口,我们可以编写可在实现该接口的任何类型上运行的代码,无论该类型代表磁盘上的文件,网络连接还是内存中的缓冲区。
例如,让JSON解码器在io.Reader上运行,比仅对文件起作用的JSON解码器功能更强大。
为了获得最大的灵活性,应尽可能编写在最少特定接口(如io.Reader或io.Writer)上操作的函数,而不是在* os.File等具体类型上运行的函数。
io.Reader
type Reader interface {
Read(p []byte) (n int, err error)
}
io.Reader是从顺序字节流中读取的关键抽象。
读取功能最多将len(p)个字节读入缓冲区p,返回读取的字节数和错误状态。
它可能返回小于len(p)的值,甚至返回0个字节。
当到达文件末尾时,它将返回io.EOF作为错误。 请注意,允许Read与返回io.EOF在同一调用中返回数据,因此处理文件结尾需要注意细节。
io.Reader不允许返回流。 为此,该类型必须实现io.Seeker或io.ReaderAt接口。
io.Writer
type Writer interface {
Write(p []byte) (n int, err error)
}
io.Writer用于写入顺序的字节流。
Write在p中写入字节,并返回写入的字节数和错误状态。
写保证它会写所有数据或返回错误,即,如果返回的n为<len(p),则err必须为非nil。
io.Closer
type Closer interface {
Close() error
}
Closer描述必须显式关闭的流。
因为在某些实际情况下需要关闭,所以关闭会返回错误。 例如,在对文件进行缓冲写入时,Close可能需要将剩余的缓冲数据刷新到文件中,这可能会失败。
因此,在关闭可写流时,请检查从关闭返回的错误,这一点很重要。
io.ReaderAt
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
io.ReaderAt类似于io.Reader,但允许在流中的任何位置进行读取。
这在文件中是可能的,但在网络连接中是不可能的。
io.WriterAt
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}
io.WriterAt类似于io.Write,但允许在流中的任意位置进行写入。
io.Seeker
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
io.Seeker允许在流中进行搜索。 如果可以查找,还可以实现io.ReaderAt和io.WriterAt。