Skip to content

Commit

Permalink
开始写第六章
Browse files Browse the repository at this point in the history
  • Loading branch information
polaris1119 committed Jun 15, 2016
1 parent 36a58c5 commit 8a9b3d6
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 3 deletions.
2 changes: 1 addition & 1 deletion chapter04/04.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@

# 导航 #

- [第二章](/chapter03/03.0.md)
- [第三章](/chapter03/03.0.md)
- 下一节:[主要类型概述](04.1.md)
18 changes: 18 additions & 0 deletions chapter06/06.0.md
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)
201 changes: 201 additions & 0 deletions chapter06/06.1.md
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)
8 changes: 6 additions & 2 deletions preface.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
- 5.2 math/big — 大数实现
- 5.3 math/cmplx — 复数基本函数操作
- 5.4 math/rand — 伪随机数生成器
* 第六章 文件系统
- 6.1 os — 平台无关的操作系统功能实现
* 第六章 [文件系统](chapter06/06.0.md)
- 6.1 [os — 平台无关的操作系统功能实现](chapter06/06.1.md)
- 6.2 path — 操作路径
- 6.3 path/filepath — 操作文件名路径
* [第七章 数据持久存储与交换](chapter07/07.0.md)
Expand All @@ -42,6 +42,10 @@
* 第九章 [测试](chapter09/09.0.md)
- 9.1 [testing - 测试基本使用接口](chapter09/09.1.md)
* 第十章 进程、线程与 goroutine
- 进程
- 线程
- goroutine
- channel
* 第十一章 网络通信与互联网 (Internet)
* 第十二章 email
* 第十三章 应用构建、debug 与测试
Expand Down

0 comments on commit 8a9b3d6

Please sign in to comment.