44 | 使用os包中的API (上)

我们今天要讲的是os代码包中的 API。这个代码包可以让我们拥有操控计算机操作系统的能力。

前导内容:os 包中的 API

这个代码包提供的都是平台不相关的 API。那么说,什么叫平台不相关的 API 呢?

它的意思是:这些 API 基于(或者说抽象自)操作系统,为我们使用操作系统的功能提供高层次的支持,但是,它们并不依赖于具体的操作系统。

不论是 Linux、macOS、Windows,还是 FreeBSD、OpenBSD、Plan9,os代码包都可以为之提供统一的使用接口。这使得我们可以用同样的方式,来操纵不同的操作系统,并得到相似的结果。

os包中的 API 主要可以帮助我们使用操作系统中的文件系统、权限系统、环境变量、系统进程以及系统信号。

其中,操纵文件系统的 API 最为丰富。我们不但可以利用这些 API 创建和删除文件以及目录,还可以获取到它们的各种信息、修改它们的内容、改变它们的访问权限,等等。

说到这里,就不得不提及一个非常常用的数据类型:os.File。

从字面上来看,os.File类型代表了操作系统中的文件。但实际上,它可以代表的远不止于此。或许你已经知道,对于类 Unix 的操作系统(包括 Linux、macOS、FreeBSD 等),其中的一切都可以被看做是文件。

除了文本文件、二进制文件、压缩文件、目录这些常见的形式之外,还有符号链接、各种物理设备(包括内置或外接的面向块或者字符的设备)、命名管道,以及套接字(也就是 socket),等等。

因此,可以说,我们能够利用os.File类型操纵的东西太多了。不过,为了聚焦于os.File本身,同时也为了让本文讲述的内容更加通用,我们在这里主要把os.File类型应用于常规的文件。

下面这个问题,就是以os.File类型代表的最基本内容入手。我们今天的问题是:os.File类型都实现了哪些io包中的接口?

这道题的典型回答是这样的。

os.File类型拥有的都是指针方法,所以除了空接口之外,它本身没有实现任何接口。而它的指针类型则实现了很多io代码包中的接口。

首先,对于io包中最核心的 3 个简单接口io.Reader、io.Writer和io.Closer,*os.File类型都实现了它们。

其次,该类型还实现了另外的 3 个简单接口,即:io.ReaderAt、io.Seeker和io.WriterAt。

正是因为*os.File类型实现了这些简单接口,所以它也顺便实现了io包的 9 个扩展接口中的 7 个。

然而,由于它并没有实现简单接口io.ByteReader和io.RuneReader,所以它没有实现分别作为这两者的扩展接口的io.ByteScanner和io.RuneScanner。

总之,os.File类型及其指针类型的值,不但可以通过各种方式读取和写入某个文件中的内容,还可以寻找并设定下一次读取或写入时的起始索引位置,另外还可以随时对文件进行关闭。

但是,它们并不能专门地读取文件中的下一个字节,或者下一个 Unicode 字符,也不能进行任何的读回退操作。

不过,单独读取下一个字节或字符的功能也可以通过其他方式来实现,比如,调用它的Read方法并传入适当的参数值就可以做到这一点。

问题解析

这个问题其实在间接地问“os.File类型能够以何种方式操作文件?”我在前面的典型回答中也给出了简要的答案。

在我进一步地说明一些细节之前,我们先来看看,怎样才能获得一个os.File类型的指针值(以下简称File值)。

在os包中,有这样几个函数,即:Create、NewFile、Open和OpenFile。

os.Create函数用于根据给定的路径创建一个新的文件。 它会返回一个File值和一个错误值。我们可以在该函数返回的File值之上,对相应的文件进行读操作和写操作。

不但如此,我们使用这个函数创建的文件,对于操作系统中的所有用户来说,都是可以读和写的。

换句话说,一旦这样的文件被创建出来,任何能够登录其所属的操作系统的用户,都可以在任意时刻读取该文件中的内容,或者向该文件写入内容。

注意,如果在我们给予os.Create函数的路径之上,已经存在了一个文件,那么该函数会先清空现有文件中的全部内容,然后再把它作为第一个结果值返回。

另外,os.Create函数是有可能返回非nil的错误值的。比如,如果我们给定的路径上的某一级父目录并不存在,那么该函数就会返回一个*os.PathError类型的错误值,以表示“不存在的文件或目录”。

再来看os.NewFile函数。 该函数在被调用的时候,需要接受一个代表文件描述符的、uintptr类型的值,以及一个用于表示文件名的字符串值。

如果我们给定的文件描述符并不是有效的,那么这个函数将会返回nil,否则,它将会返回一个代表了相应文件的File值。

注意,不要被这个函数的名称误导了,它的功能并不是创建一个新的文件,而是依据一个已经存在的文件的描述符,来新建一个包装了该文件的File值。

例如,我们可以像这样拿到一个包装了标准错误输出的File值:

file3 := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")

然后,通过这个File值向标准错误输出上写入一些内容:

if file3 != nil {
defer file3.Close()
file3.WriteString(
"The Go language program writes the contents into stderr.\n")
}

os.Open函数会打开一个文件并返回包装了该文件的File值。 然而,该函数只能以只读模式打开文件。换句话说,我们只能从该函数返回的File值中读取内容,而不能向它写入任何内容。

如果我们调用了这个File值的任何一个写入方法,那么都将会得到一个表示了“坏的文件描述符”的错误值。实际上,我们刚刚说的只读模式,正是应用在File值所持有的文件描述符之上的。

所谓的文件描述符,是由通常很小的非负整数代表的。它一般会由 I/O 相关的系统调用返回,并作为某个文件的一个标识存在。

从操作系统的层面看,针对任何文件的 I/O 操作都需要用到这个文件描述符。只不过,Go 语言中的一些数据类型,为我们隐匿掉了这个描述符,如此一来我们就无需时刻关注和辨别它了(就像os.File类型这样)。

实际上,我们在调用前文所述的os.Create函数、os.Open函数以及将会提到的os.OpenFile函数的时候,它们都会执行同一个系统调用,并且在成功之后得到这样一个文件描述符。这个文件描述符将会被储存在它们返回的File值中。

os.File类型有一个指针方法,名叫Fd。它在被调用之后将会返回一个uintptr类型的值。这个值就代表了当前的File值所持有的那个文件描述符。

不过,在os包中,除了NewFile函数需要用到它,它也没有什么别的用武之地了。所以,如果你操作的只是常规的文件或者目录,那么就无需特别地在意它了。

最后,再说一下os.OpenFile函数。 这个函数其实是os.Create函数和os.Open函数的底层支持,它最为灵活。

这个函数有 3 个参数,分别名为name、flag和perm。其中的name指代的就是文件的路径。而flag参数指的则是需要施加在文件描述符之上的模式,我在前面提到的只读模式就是这里的一个可选项。

在 Go 语言中,这个只读模式由常量os.O_RDONLY代表,它是int类型的。当然了,这里除了只读模式之外,还有几个别的模式可选,我们稍后再细说。

os.OpenFile函数的参数perm代表的也是模式,它的类型是os.FileMode,此类型是一个基于uint32类型的再定义类型。

为了加以区别,我们把参数flag指代的模式叫做操作模式,而把参数perm指代的模式叫做权限模式。可以这么说,操作模式限定了操作文件的方式,而权限模式则可以控制文件的访问权限。关于权限模式的更多细节我们将在后面讨论。

(获得 os.File 类型的指针值的几种方式)

到这里,你需要记住的是,通过os.File类型的值,我们不但可以对文件进行读取、写入、关闭等操作,还可以设定下一次读取或写入时的起始索引位置。

此外,os包中还有用于创建全新文件的Create函数,用于包装现存文件的NewFile函数,以及可被用来打开已存在的文件的Open函数和OpenFile函数。

package main

import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"syscall"
) // 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() {
// 示例1。
file1 := (*os.File)(nil)
fileType := reflect.TypeOf(file1)
var buf bytes.Buffer
fmt.Fprintf(&buf, "Type %T implements\n", file1)
for _, t := range ioTypes {
if fileType.Implements(t) {
buf.WriteString(t.String())
buf.WriteByte(',')
buf.WriteByte('\n')
}
}
output := buf.Bytes()
output[len(output)-2] = '.'
fmt.Printf("%s\n", output) // 示例2。
fileName1 := "something1.txt"
filePath1 := filepath.Join(os.TempDir(), fileName1)
var paths []string
paths = append(paths, filePath1)
dir, _ := os.Getwd()
paths = append(paths, filepath.Join(dir[:len(dir)-1], fileName1))
for _, path := range paths {
fmt.Printf("Create a file with path %s ...\n", path)
_, err := os.Create(path)
if err != nil {
var underlyingErr string
if _, ok := err.(*os.PathError); ok {
underlyingErr = "(path error)"
}
fmt.Printf("error: %v %s\n", err, underlyingErr)
continue
}
fmt.Println("The file has been created.")
}
fmt.Println() // 示例3。
fmt.Println("New a file associated with stderr ...")
file3 := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")
if file3 != nil {
file3.WriteString(
"The Go language program writes something to stderr.\n")
}
fmt.Println() // 示例4。
fmt.Printf("Open a file with path %s ...\n", filePath1)
file4, err := os.Open(filePath1)
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
fmt.Println("Write something to the file ...")
_, err = file4.WriteString("something")
var underlyingErr string
if _, ok := err.(*os.PathError); ok {
underlyingErr = "(path error)"
}
fmt.Printf("error: %v %s\n", err, underlyingErr)
fmt.Println() // 示例5。
fmt.Printf("Open a file with path %s ...\n", filePath1)
file5a, err := os.Open(filePath1)
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
fmt.Printf(
"Is there only one file descriptor for the same file in the same process? %v\n",
file5a.Fd() == file4.Fd())
file5b := os.NewFile(file5a.Fd(), filePath1)
fmt.Printf("Can the same file descriptor represent the same file? %v\n",
file5b.Name() == file5a.Name())
fmt.Println() // 示例6。
fmt.Printf("Reuse a file on path %s ...\n", filePath1)
file6, err := os.OpenFile(filePath1, os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
fmt.Printf("error: %v\n", err)
return
}
contents := "something"
fmt.Printf("Write %q to the file ...\n", contents)
n, err := file6.WriteString(contents)
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("The number of bytes written is %d.\n", n)
}
}

总结

我们今天讲的是os代码包以及其中的程序实体。我们首先讨论了os包存在的意义,和它的主要用途。代码包中所包含的 API,都是对操作系统的某方面功能的高层次抽象,这使得我们可以通过它以统一的方式,操纵不同的操作系统,并得到相似的结果。

在这个代码包中,操纵文件系统的 API 最为丰富,最有代表性的就是数据类型os.File。os.File类型不但可以代表操作系统中的文件,还可以代表很多其他的东西。尤其是在类 Unix 的操作系统中,它几乎可以代表一切可以操纵的软件和硬件。

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言实战与应用二十二)--学习笔记的更多相关文章

  1. Go语言核心36讲(Go语言实战与应用二)--学习笔记

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

  2. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

  3. Go语言核心36讲(Go语言实战与应用一)--学习笔记

    23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...

  4. Go语言核心36讲(Go语言实战与应用三)--学习笔记

    25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...

  5. Go语言核心36讲(Go语言实战与应用四)--学习笔记

    26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...

  6. Go语言核心36讲(Go语言实战与应用十二)--学习笔记

    34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...

  7. Go语言核心36讲(Go语言实战与应用十四)--学习笔记

    36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...

  8. Go语言核心36讲(Go语言实战与应用十八)--学习笔记

    40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...

  9. Go语言核心36讲(Go语言实战与应用二十四)--学习笔记

    46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...

随机推荐

  1. 通过Envoy实现.NET架构的网关

    什么是Gateway 在微服务体系结构中,如果每个微服务通常都会公开一组精细终结点,这种情况可能会有以下问题 如果没有 API 网关模式,客户端应用将与内部微服务相耦合. 在客户端应用中,单个页面/屏 ...

  2. Linux基础入门级命令文档

    Linux系统上命令的使用格式,及常用命令示例 1.命令提示符 登录系统后,第一眼看到的内容是: [root@node01 ~]# 上图就是 Linux 系统的命令提示符.那么,这个提示符的含义是什么 ...

  3. 助你上手Vue3全家桶之VueX4教程

    目录 1,前言 2,State 2.1,直接使用 2.2,结合computed 3,Getter 3.1,直接使用 3.2,结合computed 4,Mutation 4.1,直接使用 4.2,结合c ...

  4. Spring Cloud Alibaba 使用Nacos作为配置管理中心

    为什么需要配置中心? 动态配置管理是 Nacos 的三大功能之一,通过动态配置服务,我们可以在所有环境中以集中和动态的方式管理所有应用程序或服务的配置信息. 动态配置中心可以实现配置更新时无需重新部署 ...

  5. HBase的安装与部署

    一.部署前置环境 先部署分布式的高可用版的Hadoop,即ZooKeeper+Hadoop. https://www.cnblogs.com/live41/p/15483192.html * 部署的服 ...

  6. 直播预告|App 首页如何动态化更新?来看蚂蚁技术专家详解「支付宝」全新卡片技术栈

    立即前往直播间预约观看 从icon到card,一场内容前置化的变革 从 Windows 时代开始,应用程序图标就成为了用户(流量)的主入口,一直持续到移动端时代. 图标即入口的方式,虽然足够方便但却不 ...

  7. JMeter学习记录收藏

    1.如何进行一个简单的性能测试 2.JMeter各种功能名词解释,比较全 3.聚合报告分析 4.CSV文件参数化,名词解释 5.JMeter快捷键

  8. node获取请求我的客户端的地址

    node获取请求我的客户端的地址 const http = require('http'); //创建 Server const server = http.createServer() // 监听r ...

  9. CVE-2020-0796 RCE复现

    虽然热度已经过了,之前留的笔记发(水)一篇博客 影响版本 适用于32位系统的Windows 10版本1903 Windows 10 1903版(用于基于x64的系统) Windows 10 1903版 ...

  10. hudi clustering 数据聚集(二)

    小文件合并解析 执行代码: import org.apache.hudi.QuickstartUtils._ import scala.collection.JavaConversions._ imp ...