内容简介:通过os包,可以拥有操控计算机操作系统的能力。这个代码包提供的都是平台不相关的API。无论是Linux、macOS、Windows、FreeBSD、OpenBSD、Plan9,os包都可以提供统一的使用接口。这样就可以用同样的方法来操纵不同的操作系统,并得到相似的结果。os包中的API主要可以帮助我们使用操作系统中的文件系统、权限系统、环境变量、系统进程以及系统信号。其中,文件系统的API最丰富。不但可以用来创建和删除文件以及目录,还可以获取到各种信息、修改内容、修改访问权限。等等。这里,最常用的数据类型就
os包
通过os包,可以拥有操控计算机操作系统的能力。这个代码包提供的都是平台不相关的API。无论是 Linux 、macOS、Windows、FreeBSD、OpenBSD、Plan9,os包都可以提供统一的使用接口。这样就可以用同样的方法来操纵不同的操作系统,并得到相似的结果。
os包中的API主要可以帮助我们使用操作系统中的文件系统、权限系统、环境变量、系统进程以及系统信号。其中,文件系统的API最丰富。不但可以用来创建和删除文件以及目录,还可以获取到各种信息、修改内容、修改访问权限。等等。这里,最常用的数据类型就是:os.File。
os.File类型介绍
从字面上看,os.File类型代表了操作系统中的文件,但是实际上,它代表的远不止于此。比如对于类Unix的操作系统,包括Linux、macOS、FreeBSD等,其中的一切都可以被看作是文件。
除了文本文件、二进制文件、压缩文件、目录这些常见的形式之外,还有符号链接、各种物理设备(包括内置或外接的面向块或者字符的设备)、命名管道,以及套接字(也就是socket),等等。所以能够利用os.File类型操纵的东西有很多。不过接下来主要介绍os.File类型应用于常规文件。
实现的io接口
os.File类型拥有的都是指针方法,它的指针实现了很多io包中的接口。
对于io包中最核心的3个简单接口io.Reader、io.Writer和io.Closer,*os.File类型都实现了它们。另外还顺便实现了io包中的9个扩展接口中的7个。没有实现简单接口io.ByteReader和io.RuneReader,所以也没有实现上面这两个的扩展接口io.ByteScannser和io.RuneScanner。
总之,os.File类型及其指针类型的值,不但可以通过各种方式读取和写入某个文件中的内容,还可以寻找并设定下一次读取或写入时的起始索引位置,另外还可以随时对文件进行关闭。但是,它并不能专门的读取文件中的下一个字节或Unicode字符,也不能进行任何的读回退操作。不过,单读读取下一个字节或字符的功能也可以通过其他方式来实现。比如用Read方法传入适当的参数。
使用反射检查接口的实现下面的示例枚举了io包中的所有接口,检查*os.File是否实现了该接口:
package main import ( "bytes" "fmt" "io" "os" "reflect" ) // ioTypes 代表了io代码包中的所有接口的反射类型。 var ioTypes = []reflect.Type{ reflect.TypeOf((*io.Reader)(nil)).Elem(), reflect.TypeOf((*io.Writer)(nil)).Elem(), reflect.TypeOf((*io.Closer)(nil)).Elem(), reflect.TypeOf((*io.ByteReader)(nil)).Elem(), reflect.TypeOf((*io.RuneReader)(nil)).Elem(), reflect.TypeOf((*io.ReaderAt)(nil)).Elem(), reflect.TypeOf((*io.Seeker)(nil)).Elem(), reflect.TypeOf((*io.WriterTo)(nil)).Elem(), reflect.TypeOf((*io.ByteWriter)(nil)).Elem(), reflect.TypeOf((*io.WriterAt)(nil)).Elem(), reflect.TypeOf((*io.ReaderFrom)(nil)).Elem(), reflect.TypeOf((*io.ByteScanner)(nil)).Elem(), reflect.TypeOf((*io.RuneScanner)(nil)).Elem(), reflect.TypeOf((*io.ReadSeeker)(nil)).Elem(), reflect.TypeOf((*io.ReadCloser)(nil)).Elem(), reflect.TypeOf((*io.WriteCloser)(nil)).Elem(), reflect.TypeOf((*io.WriteSeeker)(nil)).Elem(), reflect.TypeOf((*io.ReadWriter)(nil)).Elem(), reflect.TypeOf((*io.ReadWriteSeeker)(nil)).Elem(), reflect.TypeOf((*io.ReadWriteCloser)(nil)).Elem(), } func main() { var file os.File fileType := reflect.TypeOf(&file) var buf bytes.Buffer // 存放没有实现的那些接口信息,最后统一打印出来 fmt.Fprintf(&buf, "Type %T not implements:\n", &file) fmt.Printf("Type %T implements:\n", &file) for _, t := range ioTypes { if fileType.Implements(t) { fmt.Println(t.String()) } else { fmt.Fprintln(&buf, t.String()) } } fmt.Println() fmt.Println(buf.String()) }
一般要检查接口是否实现了,不需要用到反射这么高级的用法。
操作文件
要操作文件,首先要获取一个os.File类型的指针值,简称File值。在os包中,有如下几个函数:
- Create
- NewFile
- Open
- OpenFile
os.Create函数
用于根据给定的路径创建一个新的文件。它会返回一个File值和一个错误值。可以在该函数返回的File值之上,对相应的文件进行读操作和写操作。使用这个函数创建的文件,对操作系统中的所有用户来说都是可以读和写的。
注意,如果给定的路径已经存在一个文件了,那么该函数会先清空现有文件中的内容,然后再把该文件的File值返回。就是覆盖原有文件创建一个新的空文件。另外,如果有错误,会通过第二个参数返回错误值。比如,如果路径不存在,那么会返回一个*os.PathErro类型的错误值。
下面的示例,尝试当在前目录下创建一个文件。还会把当前目录的名称截掉最后一个字符,这应该会是一个不存在的目录,同样尝试创建一个文件,然后会返回一个预期的错误:
package main import ( "fmt" "os" "path/filepath" ) func main() { tempPath := os.TempDir() fmt.Println("系统的临时文件夹:", tempPath) fileName := "test.txt" var paths []string dir, _ := os.Getwd() dirPath := filepath.Join(dir, fileName) // 在当前文件夹下创建一个文件 paths = append(paths, dirPath) notExistsPath := filepath.Join(dir[:len(dir)-1], fileName) // 这个文件夹路径应该不存在 paths = append(paths, notExistsPath) for _, path := range paths { fmt.Println("创建文件:", path) _, err := os.Create(path) // 返回的第一个参数是*File,就不要的 if err != nil { var underlyingErr string if _, ok := err.(*os.PathError); ok { underlyingErr = "(path error)" } fmt.Fprintf(os.Stderr, "ERROR: %v %s\n", err, underlyingErr) continue } fmt.Println("创建文件成功.") } }
os.NewFile函数
该函数在被调用的时候需要接受一个代表文件描述符的uintptr类型的值,以及一个用于表示文件名的字符串。如果给定的不是有效的文件描述符,那么会返回nil。否则,返回相应文件的File值。这里不要被函数名称误导,它的功能不是创建一个新的文件,而是依据一个已经存在的文件的描述符,来新建一个包装了该文件的File值。比如,可以像这样拿到一个包装了标准错误输出的File值:
file := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")
然后,通过这个File值向标准错误输出写入一些内容,一般的效果就是打印出错误信息:
if file != nil { file.WriteString("Test Stderr.\n") }
如果是一个已存在的文件,可以使用该文件的文件描述符作为函数的第一个参数返回File值。下面文件描述符的内容里还有一个示例。
os.Open函数
打开一个文件并返回包装了该文件的File值。该函数只能以只读模式打开文件。如果调用了File值的任何写入方法,都会返回错误:
package main import ( "fmt" "os" "path/filepath" ) func main() { fileName := "test.txt" dir, _ := os.Getwd() dirPath := filepath.Join(dir, fileName) file, err := os.Open(dirPath) if err != nil { // 文件可能不存在,先创建一个文件 fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) return } _, err = file.WriteString(" ") // 文件是只读的,尝试写入会返回错误 var underlyingErr string if _, ok := err.(*os.PathError); ok { underlyingErr = "(path error)" } fmt.Fprintf(os.Stderr, "ERROR: %v %s\n", err, underlyingErr) }
文件描述符
实际上,上面说的只读模式,正是应用在File值所持有的文件描述符之上的。
文件描述符,是由通常很小的非负整数代表的。它一般会由I/O相关的系统调用返回,并作为某个文件的一个表示存在。
从操作系统的层面看,针对任何文件的I/O操作都需要用到这个文件描述符。只不过,Go语言中的一些数据类型,为我们隐匿掉了这个描述符。实际上,在调用os.Create函数、os.Open函数以及之后会提到的os.OpenFile函数时,都会执行同一个系统调用,并且在成功之后会得到这样一个文件描述符。这个文件描述符将会被存储在返回的File值中。os.File类型有一个Fd的指针方法,返回一个uintptr类型的值。这个值就代表了当前的File值所持有的那个文件描述符。
不过在os包中,只有NewFile函数需要用到它。所以,如果操作的只是常规的文件或目录,也无需特别在意。
文件描述符相关的示例:
package main import ( "fmt" "os" "path/filepath" ) func main() { fileName := "test.txt" dir, _ := os.Getwd() dirPath := filepath.Join(dir, fileName) file1, err := os.Open(dirPath) if err != nil { // 文件可能不存在,先创建一个文件 fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) return } file2, _ := os.Open(dirPath) // 虽然打开的是同一个文件,但却是不同的文件描述符 file3 := os.NewFile(file1.Fd(), dirPath) // 可以通过文件描述符,获取File值 fmt.Println(file1.Fd(), file2.Fd(), file3.Fd()) }
通过Fd方法获取到的文件描述符可以通过os.NewFile函数返回File值。
os.OpenFile函数
这个函数其实是os.Create函数和os.Open函数的底层支持,它最灵活。这个函数有3个参数:
func OpenFile(name string, flag int, perm FileMode) (*File, error) { testlog.Open(name) return openFileNolog(name, flag, perm) }
name参数,是文件的路径。
flag参数,是需要施加在文件描述符上的模式,叫操作模式,Open函数是只读的就是因为在Open函数里调用OpenFile的时候指定了该参数:
func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) }
perm参数,也是模式,叫权限模式。类型是os.FileMode,此类型是一个基于uint32类型的再定义类型:
type FileMode uint32
这里的两个模式:
- flag : 操作模式,限定了操作文件方式
- perm : 权限模式,控制文件的访问权限
关于操作模式和访问权限的更多细节,在后面继续讲。
打开文件并写入内容的操作示例:
package main import ( "fmt" "os" "path/filepath" ) func main() { fileName := "test.txt" dir, _ := os.Getwd() dirPath := filepath.Join(dir, fileName) // O_WRONLY:只写模式。O_CREATE:文件不存在就创建。O_TRUNC:打开并清空文件 file, err := os.OpenFile(dirPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) return } n, err := file.WriteString("写入操作") if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } else { fmt.Println("写入字节数(bytes):", n) } }
操作模式
针对File值的操作模式主要有:
- 只读模式:os.O_RNONLY
- 只写模式:os.O_WRONLY
- 读写模式:os.O_RDWR
在新建一个文件的时候,必须把这三个模式中的一个设定为此文件的操作模式。
另外,还可以再设置额外的操作模式,选项如下:
- os.O_APPEND : 写入时追加到现有内容的后边
- os.O_CREARE : 当文件不存在是,创建一个新文件
- os.O_EXCL : 需要与os.O_CREATE一同使用,表示给定的路径不能是一个已存在的文件。(就是指定的文件必须不存在,然后创建文件)
- os.O_SYNC : 在打开的文件之上实施同步I/O。它会保证读写的内容总会与硬盘上的数据保持同步
- os.O_TRUNC : 如果文件已存在,并且是常规文件,就先清空其中的内容。(就是创建文件,如果文件存在则新建并覆盖)
操作模式的使用
对于以上操作模式的使用,os.Open函数和os.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) }
这里顺便也是下面访问权限的例子了。
这里可以看到,多个操作符是通过按位或操作符(|)组合起来的,常用的写模式的组合还有:
os.O_WRONLY|os.O_CREATE|os.O_EXCL os.O_WRONLY|os.O_CREATE|os.O_TRUNC os.O_WRONLY|os.O_APPEND
访问权限
os.OpenFile函数的第三个参数perm代表的是权限模式,类型是os.FileMode。实际上,os.FIleMode类型能够代表的,不只权限模式,还可以代表文件模式,也可以称之为文件种类。
os.FileMode是基于uint32类型的再定义类型,它包含了32个比特位。在这32个比特位中,每个比特位都有其特定的意义:
- 如果最高位上的二进制数是1,那么该值的文件模式等同于os.ModeDir,这代表是一个目录。
- 如果第26个比特位是1,那么该值的文件模式等同于os.ModeNamedPipe,这代表是一个命名管道。
- 最低的9个比特位,这几位才用于表示文件的权限。这个权限参考Linux的ugo权限。
所有的常量都在源码里有说明:
const ( // The single letters are the abbreviations // used by the String method's formatting. ModeDir FileMode = 1 << (32 - 1 - iota) // d: is a directory ModeAppend // a: append-only ModeExclusive // l: exclusive use ModeTemporary // T: temporary file; Plan 9 only ModeSymlink // L: symbolic link ModeDevice // D: device file ModeNamedPipe // p: named pipe (FIFO) ModeSocket // S: Unix domain socket ModeSetuid // u: setuid ModeSetgid // g: setgid ModeCharDevice // c: Unix character device, when ModeDevice is set ModeSticky // t: sticky ModeIrregular // ?: non-regular file; nothing else is known about this file // Mask for the type bits. For regular files, none will be set. ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeIrregular ModePerm FileMode = 0777 // Unix permission bits )
可以像操作模式那样用按位或操作符(|)组合起来。一般也就直接0666或0777就好了。
补充-文件读写
这篇还是偏重理论,主要讲的打开文件的操作。在打开文件获取到os.File类型后,由于它的指针已经实现了各种io接口,之后的读写操作,就是通过调用*os.File实现的io接口来实现了。另外,io包还有一个ioutil子包,可以读取整个文件。而逐行读取文件的内容,也需要一些具体的实现。
下面这篇有一些文件读写的示例:
http://blog.51cto.com/steed/2315597以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。