25.Go 文件和I/O

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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容