-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
36a58c5
commit 8a9b3d6
Showing
4 changed files
with
226 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,5 +11,5 @@ | |
|
||
# 导航 # | ||
|
||
- [第二章](/chapter03/03.0.md) | ||
- [第三章](/chapter03/03.0.md) | ||
- 下一节:[主要类型概述](04.1.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# 第六章 文件系统 # | ||
|
||
Go 的标准库提供了很多工具,可以处理文件系统中的文件、构造和解析文件名等。 | ||
|
||
处理文件的第一步是确定要处理的文件的名字。Go 将文件名表示为简单的字符串,提供了 `path`、`filepath` 等库来操作文件名或路径。用 `os` 中 `File` 结构的 `Readdir` 可以列出一个目录中的内容。 | ||
|
||
可以用 `os.Stat` 或 `os.Lstat` 来检查文件的一些特性,如权限、大小等。 | ||
|
||
有时需要创建草稿文件来保存临时数据,或将数据移动到一个永久位置之前需要临时文件存储,`os.TempDir` 可以返回默认的临时目录,用于存放临时文件。关于临时文件,在 `ioutil` 中已经讲解了。 | ||
|
||
`os` 包还包含了很多其他文件系统相关的操作,比如创建目录、重命名、移动文件等等。 | ||
|
||
由于本章探讨文件系统相关知识,`os` 包中关于进程相关的知识会在后续章节讲解。 | ||
|
||
# 导航 # | ||
|
||
- [第五章](/chapter05/05.0.md) | ||
- 下一节:[os — 平台无关的操作系统功能实现](06.1.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
# 6.1 os — 平台无关的操作系统功能实现 # | ||
|
||
`os` 包提供了平台无关的操作系统功能接口。尽管错误处理是 go 风格的,但设计是 Unix 风格的;所以,失败的调用会返回 `error` 而非错误码。通常 `error` 里会包含更多信息。例如,如果使用一个文件名的调用(如Open、Stat)失败了,打印错误时会包含该文件名,错误类型将为`*PathError`,其内部可以解包获得更多信息。 | ||
|
||
os包的接口规定实现为在所有操作系统中都是一致的。有一些某个系统特定的功能,需要使用 `syscall` 获取。实际上,`os` 依赖于 `syscall`。在实际编程中,我们应该总是优先使用 `os` 中提供的功能,而不是 `syscall`。 | ||
|
||
下面是一个简单的例子,打开一个文件并从中读取一些数据: | ||
|
||
``` | ||
file, err := os.Open("file.go") // For read access. | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
``` | ||
如果打开失败,错误字符串是自解释的,例如: | ||
|
||
`open file.go: no such file or directory` | ||
|
||
而不像 C 语言,需要额外的函数(或宏)来解释错误码。 | ||
|
||
## 文件 I/O | ||
|
||
在第一章,我们较全面的介绍了 Go 中的 I/O。本节,我们着重介绍文件相关的 I/O。因为 I/O 操作涉及到系统调用,在讲解时会涉及到 Unix 在这方面的系统调用。 | ||
|
||
在 Unix 系统调用中,所有执行 I/O 操作以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。这里,我们主要介绍普通文件的 I/O。 | ||
|
||
在 Go 中,文件描述符封装在 `os.File` 结构中,通过 `File.Fd()` 可以获得底层的文件描述符:fd。 | ||
|
||
按照惯例,大多数程序都期望能够使用 3 种标准的文件描述符:0-标准输入;1-标准输出;2-标准错误。`os` 包提供了 3 个 `File` 对象,分别代表这 3 种标准描述符:`Stdin`、`Stdout` 和 `Stderr`,它们对应的文件名分别是:/dev/stdin、/dev/stdout 和 /dev/stderr。注意,这里说的文件名,并不说一定存在的文件名,比如 Windows 下就没有。 | ||
|
||
### 打开一个文件:OpenFile | ||
|
||
`OpenFile` 既能打开一个已经存在的文件,也能创建并打开一个新文件。 | ||
|
||
`func OpenFile(name string, flag int, perm FileMode) (*File, error)` | ||
|
||
`OpenFile` 是一个更一般性的文件打开函数,大多数调用者都应用 `Open` 或 `Create` 代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于 I/O。如果出错,错误底层类型是 `*PathError`。 | ||
|
||
要打开的文件由参数 `name` 指定,它可以是绝对路径或相对路径(相对于进程当前工作目录),也可以是一个符号链接(会对其进行解引用)。 | ||
|
||
位掩码参数 `flag` 用于指定文件的访问模式,可用的值在 `os` 中定义为常量(以下值并非所有操作系统都可用): | ||
|
||
``` | ||
const ( | ||
O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件 | ||
O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件 | ||
O_RDWR int = syscall.O_RDWR // 读写模式打开文件 | ||
O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部 | ||
O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件 | ||
O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用,文件必须不存在 | ||
O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O | ||
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件 | ||
) | ||
``` | ||
其中,`O_RDONLY`、`O_WRONLY`、`O_RDWR` 应该只指定一个,剩下的通过 `|` 操作符来指定。该函数内部会给 `flags` 加上 `syscall.O_CLOEXEC`,在 fork 子进程时会关闭通过 `OpenFile` 打开的文件,即子进程不会重用该文件描述符。 | ||
|
||
*注意:由于历史原因,`O_RDONLY | O_WRONLY` 并非等于 `O_RDWR`,它们的值一般是 0、1、2。* | ||
|
||
位掩码参数 `perm` 指定了文件的模式和权限位,类型是 `os.FileMode`,文件模式位常量定义在 `os` 中: | ||
|
||
``` | ||
const ( | ||
// 单字符是被 String 方法用于格式化的属性缩写。 | ||
ModeDir FileMode = 1 << (32 - 1 - iota) // d: 目录 | ||
ModeAppend // a: 只能写入,且只能写入到末尾 | ||
ModeExclusive // l: 用于执行 | ||
ModeTemporary // T: 临时文件(非备份文件) | ||
ModeSymlink // L: 符号链接(不是快捷方式文件) | ||
ModeDevice // D: 设备 | ||
ModeNamedPipe // p: 命名管道(FIFO) | ||
ModeSocket // S: Unix域socket | ||
ModeSetuid // u: 表示文件具有其创建者用户id权限 | ||
ModeSetgid // g: 表示文件具有其创建者组id的权限 | ||
ModeCharDevice // c: 字符设备,需已设置ModeDevice | ||
ModeSticky // t: 只有root/创建者能删除/移动文件 | ||
// 覆盖所有类型位(用于通过&获取类型位),对普通文件,所有这些位都不应被设置 | ||
ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ||
ModePerm FileMode = 0777 // 覆盖所有Unix权限位(用于通过&获取类型位) | ||
) | ||
``` | ||
以上常量在所有操作系统都有相同的含义(可用时),因此文件的信息可以在不同的操作系统之间安全的移植。不是所有的位都能用于所有的系统,唯一共有的是用于表示目录的 `ModeDir` 位。 | ||
|
||
以上这些被定义的位是 `FileMode` 最重要的位。另外 9 个位(权限位)为标准 Unix rwxrwxrwx 权限(所有人都可读、写、运行)。 | ||
|
||
`FileMode` 还定义了几个方法,用于判断文件类型的 `IsDir()` 和 `IsRegular()`,用于获取权限的 `Perm()`。 | ||
|
||
返回的 `error`,具体实现是 `*os.PathError`,它会记录具体操作、文件路径和错误原因。 | ||
|
||
另外,在 `OpenFile` 内部会调用 `NewFile`,来得到 `File` 对象。 | ||
|
||
**使用方法** | ||
|
||
打开一个文件,一般通过 `Open` 或 `Create`,我们看这两个函数的实现。 | ||
|
||
``` | ||
func Open(name string) (*File, error) { | ||
return OpenFile(name, O_RDONLY, 0) | ||
} | ||
func Create(name string) (*File, error) { | ||
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) | ||
} | ||
``` | ||
|
||
### 读取文件内容:Read | ||
|
||
`func (f *File) Read(b []byte) (n int, err error)` | ||
|
||
`Read` 方法从 `f` 中读取最多 `len(b)` 字节数据并写入 `b`。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取0个字节且返回值 err 为 `io.EOF`。 | ||
|
||
从方法声明可以知道,`File` 实现了 `io.Reader` 接口。 | ||
|
||
`Read` 对应的系统调用是 `read`。 | ||
|
||
对比下 `ReadAt` 方法: | ||
|
||
`func (f *File) ReadAt(b []byte, off int64) (n int, err error)` | ||
|
||
`ReadAt` 从指定的位置(相对于文件开始位置)读取长度为 `len(b)` 个字节数据并写入 `b`。它返回读取的字节数和可能遇到的任何错误。当 n<len(b) 时,本方法总是会返回错误;如果是因为到达文件结尾,返回值err 会是 `io.EOF`。它对应的系统调用是 `pread`。 | ||
|
||
**`Read` 和 `ReadAt` 的区别**:前者从文件当前偏移量处读,且会改变文件当前的偏移量;而后者从 `off` 指定的位置开始读,且不会改变文件当前偏移量。 | ||
|
||
### 数据写入文件:Write | ||
|
||
`func (f *File) Write(b []byte) (n int, err error)` | ||
|
||
`Write` 向文件中写入 `len(b)` 字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值 `n!=len(b)`,本方法会返回一个非nil的错误。 | ||
|
||
从方法声明可以知道,`File` 实现了 `io.Writer` 接口。 | ||
|
||
`Write` 对应的系统调用是 `write`。 | ||
|
||
`Write` 与 `WriteAt` 的区别同 `Read` 与 `ReadAt` 的区别一样。 | ||
|
||
注意:`Write` 调用成功并不能保证数据已经写入磁盘,因为内核会缓存磁盘的 I/O 操作。如果希望立刻将数据写入磁盘(一般场景不建议这么做,因为会影响性能),有两种办法: | ||
|
||
1. 打开文件时指定 `os.O_SYNC`; | ||
2. 调用 `File.Sync()` 方法。 | ||
|
||
说明:`File.Sync()` 底层调用的是 `fsync` 系统调用,这会将数据和元数据都刷到磁盘;如果只想刷数据到磁盘(比如,文件大小没变,只是变了文件数据),需要自己封装,调用 `fdatasync` 系统调用。(`syscall.Fdatasync`) | ||
|
||
### 关闭文件:Close | ||
|
||
`close()` 系统调用关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。当进程终止时,将自动关闭其已打开的所有文件描述符。 | ||
|
||
`func (f *File) Close() error` | ||
|
||
`os.File.Close()` 是对 `close()` 的封装。我们应该养成关闭不需要的文件的良好编程习惯。文件描述符是资源,Go 的 gc 是针对内存的,并不会自动回收资源,如果关闭文件描述符,长期运行的服务可能会把文件描述符耗尽。 | ||
|
||
所以,通常的写法如下: | ||
|
||
``` | ||
file, err := os.Open("/tmp/studygolang.txt") | ||
if err != nil { | ||
// 错误处理,一般会阻止程序往下执行 | ||
return | ||
} | ||
defer file.Close() | ||
``` | ||
|
||
**关于返回值 `error`** | ||
|
||
以下两种情况会导致 `Close` 返回错误: | ||
|
||
1. 关闭一个未打开的文件; | ||
2. 两次关闭同一个文件; | ||
|
||
通常,我们不回去检查 `Close` 的错误。 | ||
|
||
### 改变文件偏移量:Seek | ||
|
||
对于每个打开的文件,系统内核会记录其文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指执行下一个 `Read` 或 `Write` 操作的文件其实位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为 0。 | ||
|
||
文件打开时,会将文件偏移量设置为指向文件开始,以后每次 `Read` 或 `Write` 调用将自动对其进行调整,以指向已读或已写数据后的下一个字节。因此,连续的 `Read` 和 `Write` 调用将按顺序递进,对文件进行操作。 | ||
|
||
而 `Seek` 可以调整文件偏移量。方法定义如下: | ||
|
||
`func (f *File) Seek(offset int64, whence int) (ret int64, err error)` | ||
|
||
`Seek` 设置下一次读/写的位置。offset 为相对偏移量,而 whence 决定相对位置:0为相对文件开头,1为相对当前位置,2为相对文件结尾。它返回新的偏移量(相对开头)和可能的错误。使用中,whence 应该使用 `os` 包中的常量:`SEEK_SET`、`SEEK_CUR` 和 `SEEK_END`。 | ||
|
||
注意:`Seek` 只是调整内核中与文件描述符相关的文件偏移量记录,并没有引起对任何物理设备的访问。 | ||
|
||
一些 `Seek` 的使用例子(file 为打开的文件对象),注释说明了将文件偏移量移动到的具体位置: | ||
|
||
``` | ||
file.Seek(0, os.SEEK_SET) // 文件开始处 | ||
file.Seek(0, SEEK_END) // 文件结尾处的下一个字节 | ||
file.Seek(-1, SEEK_END) // 文件最后一个字节 | ||
file.Seek(-10, SEEK_CUR) // 当前位置前10个字节 | ||
file.Seek(1000, SEEK_END) // 文件结尾处的下1001个字节 | ||
``` | ||
最后一个例子在文件中会产生“空洞”。 | ||
|
||
`Seek` 对应系统调用 `lseek`。该系统调用并不适用于所有类型,不允许将 `lseek ` 应用于管道、FIFO、socket 或 终端。 | ||
|
||
# 导航 # | ||
|
||
- [第六章](/chapter06/06.0.md) | ||
- 下一节:[path — 操作路径](06.2.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters