Go 下的
os
package 实现类似unix
的.所以我想出了一遍啃 APUE 中的基本IO
和翻阅os
下有关基本IO的源码的方式来武装自己.
基本文件 IO 的全局观
首先我们想想,我们平时都对文件进行了哪些操作?
以老生常谈的 Hello World
为栗:
创建一个 hello_world
文件,然后用你喜欢的语言编写实现代码,然后保存运行.就这个简单的操作,对应的基本文件 IO 操作分别是:Create, Seek, Write, Close, Open, Seek, Read, Close.
如此引出了基本文件 IO 操作:Create
, Open
, Read
, Write
, Seek
, Close
,希望如上的栗子可以加深对基本文件操作的印象.
常见的操作类型常量:
- O_RDONLY:只读
- O_WRONLY:只写
- O_RDWWR:读,写
- O_EXEC:执行
- .......
文件访问权限:
- 用户读,写,执行
- 组读,写,执行
- 其他读,写,执行
大体有个概念就好,学习是螺旋上升的状态,也许在某个时间端就有更深刻的理解了。
逐个击破
在 Go 的角度,认识下它们,俗话说"知己知彼,百战不殆".一方面用的时候可以手到擒来,另一方面则循序渐进的掌握其设计思想.
Create
首先我们得会用它,如下代码所示:
//func Create(name string) (*File, error)
file, err := os.Create("filename")
if err != nil {
//...
}
// file...
调用 Create
函数后,会返回 文件表示符
和 *PathError
, 如果没有发生异常,*PathError
则为空.
因为 PathError
很简单,随便看下其源码:
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
通过 PathError
的结构体可知,其起到了提示信息的作用.如遇到 Error, 则返回当前文件的操作类型,路径和错误信息.
回到文件描述符
(File)上来,通过递归查看源码得到如下结果:
// File represents an open file descriptor.
type File struct {
*file // os specific
}
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
fd int
name string
dirinfo *dirInfo // nil unless directory being read
}
// Auxiliary information if the File describes a directory
type dirInfo struct {
buf []byte // buffer for directory I/O
nbuf int // length of buf; return value from Getdirentries
bufp int // location of next record in buf.
}
这里只讨论基本文件IO,所以 dirInfo
先略过,所以我们大体了解了其 File 的数据结构。
看下 func Create(name string) (*File, error)
的源码:
// Create creates the named file with mode 0666 (before umask), truncating
// it if it already exists. If successful, methods on the returned
// File can be used for I/O; the associated file descriptor has mode
// O_RDWR.
// If there is an error, it will be of type *PathError.
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
其实它是调用 OpenFile
这个函数,然而我们不再往下深究啦,目前我们先要有个广度。这里就引出了常用的操作类型
和文件访问权限
。
为了更加印象深刻,这里不一一列举啦,希望能在以后的实际场景中能够遇到。
Open
把握住三个点,打开的文件,做什么操作,权限是多少。
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
从源码中看出,其实围绕这三个关注点调用 OpenFile
函数,进而进行系统调用,完成 Open
操作。可以稍微留意下 O_RDONLY
操作类型和 0
的访问权限
Read
Go 提供的 Read
有两种读取方式,一种是指定起始偏移量来读取数据,另一种不能指定。
// ReadAt reads len(b) bytes from the File starting at byte offset off.
// It returns the number of bytes read and the error, if any.
// ReadAt always returns a non-nil error when n < len(b).
// At end of file, that error is io.EOF.
func (f *File) ReadAt(b []byte, off int64) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
for len(b) > 0 {
m, e := f.pread(b, off)
if m == 0 && e == nil {
return n, io.EOF
}
if e != nil {
err = &PathError{"read", f.name, e}
break
}
n += m
b = b[m:]
off += int64(m)
}
return
}
代码中关键的语句:m, e := pread(b, off)
, 使方法可以指定 offset
读取。
// Read reads up to len(b) bytes from the File.
// It returns the number of bytes read and any error encountered.
// At end of file, Read returns 0, io.EOF.
func (f *File) Read(b []byte) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
if n == 0 && len(b) > 0 && e == nil {
return 0, io.EOF
}
if e != nil {
err = &PathError{"read", f.name, e}
}
return n, err
}
与上面对比,很明显发现 n, e := f.read(b)
, 不需要指定 offset
, 相当于内嵌了一个 current pointer
似的。
Write
Read 和 Write 仅是在操作上不同,一个是读,一个是写。然而其运行结构却相同,二者之分如 read
, 详情请品悦源码吧。
// Write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
// Write returns a non-nil error when n != len(b).
func (f *File) Write(b []byte) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
n, e := f.write(b)
if n < 0 {
n = 0
}
if n != len(b) {
err = io.ErrShortWrite
}
epipecheck(f, e)
if e != nil {
err = &PathError{"write", f.name, e}
}
return n, err
}
// WriteAt writes len(b) bytes to the File starting at byte offset off.
// It returns the number of bytes written and an error, if any.
// WriteAt returns a non-nil error when n != len(b).
func (f *File) WriteAt(b []byte, off int64) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
for len(b) > 0 {
m, e := f.pwrite(b, off)
if e != nil {
err = &PathError{"write", f.name, e}
break
}
n += m
b = b[m:]
off += int64(m)
}
return
}
Seek
Seek 可以看出文件偏移指针。对参数 offset 的解释与参数 whence 的值相关:
- 若 whence 是 SEEK_SET, 则将该文件的偏移量设置为距文件开始处 offset 个字节
- 若 whence 是 SEEK_CUR, 则将该文件的偏移量设置为其当前值加 offset, offset 可为正或负
- 若 whence 是 SEEK_END, 则将该文件的偏移量设置为文件长度加 offset, offset 可为正或负
有兴趣的,可以看看源码:
// Seek sets the offset for the next Read or Write on file to offset, interpreted
// according to whence: 0 means relative to the origin of the file, 1 means
// relative to the current offset, and 2 means relative to the end.
// It returns the new offset and an error, if any.
// The behavior of Seek on a file opened with O_APPEND is not specified.
func (f *File) Seek(offset int64, whence int) (ret int64, err error) {
if err := f.checkValid("seek"); err != nil {
return 0, err
}
r, e := f.seek(offset, whence)
if e == nil && f.dirinfo != nil && r != 0 {
e = syscall.EISDIR
}
if e != nil {
return 0, &PathError{"seek", f.name, e}
}
return r, nil
}
Close
这个操作,在使用文件的时候千万不要忘记,以防浪费资源。
// Close closes the File, rendering it unusable for I/O.
// It returns an error, if any.
func (f *File) Close() error {
if f == nil {
return ErrInvalid
}
return f.file.close()
}
当进度停歇时,我只好想到这个办法来推进,距离 2018 年还有 99 天,我希望我能啃完这本 APUE.
精彩文章,持续更新,请关注微信公众号: