前言
在开发过程中经常需要字符串处理,文件处理等操作。遇到这些问题时我经常都需要上网现查, 感觉很影响效率,也反映我对标准库的掌握还不够。所以,我今天特地又详细阅读了《Go语言标准库》, 并对常用的函数和方法做了这个摘录,也方便后期的复习和查阅。
io包
这是基本的io包,后面介绍的几个包都是在它的基础上封装而来的。它主要提供io的接口而不是实现, 所以我们一般使用实现了io包接口的几个常用包。
除了下面介绍的接口之外,还有读写单个字节的接口,用的不多;ReadCloser、ReadSeeker、ReadWriteCloser、 ReadWriteSeeker、ReadWriter、WriteCloser 和WriteSeeker 这些接⼝是主要由它们直接组合⽽成的新接⼝; 其他的一些接口用的很少。这些都不再详细介绍。
Reader 和Writer 接⼝
常⽤的至少实现了它们之一的类型有:os.File、strings.Reader、 bufio.Reader/Writer、bytes.Buffer、bytes.Reader
后面的接口都可以看做是围绕在Reader和Writer这两个接口类型上扩展而来的。
Reader 接⼝的定义如下:
type Reader interface { Read(p []byte) (n int, err error) }
官⽅⽂档中关于该接⼝⽅法的说明:
Read 将 len(p) 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <=len(p)) 以及任何遇到的错误。
即使 Read 返回的 n < len(p),它也会在调⽤过程中使⽤ p 的全部作为暂存空间。若⼀些数据可⽤但不到 len(p) 个字节, Read会照例返回可⽤的数据,⽽不是等待更多数据。当 Read 在成功读取 n > 0 个字节后遇到⼀个错误或 EOF (end-of-file), 它就会返回读取的字节数。它会从相同的调⽤中返回(⾮nil的)错误或从随后的调⽤中返回错误(同时 n == 0)。
⼀般情况的⼀个例⼦就是 Reader 在输⼊流结束时会返回⼀个⾮零的字节数,同时返回的 err 不是 EOF 就是 nil。
⽆论如何,下⼀个 Read 都应当返回 0, EOF。调⽤者应当总在考虑到错误 err 前处理 n > 0 的字节。 这样做可以在读取⼀些字节,以及允许的 EOF ⾏为后正确地处理 I/O 错误。
Writer 接⼝的定义如下:
type Writer interface { Write(p []byte) (n int, err error) }
官⽅⽂档中关于该接⼝⽅法的说明:
Write 将 len(p) 个字节从 p 中写⼊到基本数据流中。它返回从 p 中被写⼊的字 节数 n(0 <= n <= len(p))以及任何遇到的引起写⼊提前停⽌的错误。若 Write 返回的 n < len(p),它就必须返回⼀个 ⾮nil 的错误。
ReaderAt 和WriterAt 接⼝
这两个接口主要是提供了带偏移量(offset)的io操作。
第一个接口定义如下:
type ReaderAt interface { ReadAt(p []byte, off int64) (n int, err error) }
官⽅⽂档中关于该接⼝⽅法的说明:
ReadAt 从基本输⼊源的偏移量 off 处开始,将 len(p) 个字节读取到 p 中。它返 回读取的字节数 n(0 <= n <= len(p))以及任何遇到的错误。
当 ReadAt 返回的 n < len(p) 时,它就会返回⼀个 ⾮nil 的错误来解释 为什么没 有返回更多的字节。在这⼀点上,ReadAt ⽐ Read 更严格。
即使 ReadAt 返回的 n < len(p),它也会在调⽤过程中使⽤ p 的全部作为暂存空 间。若⼀些数据可⽤但不到 len(p) 字节,ReadAt 就会阻塞直到所有数据都可⽤ 或产⽣⼀个错误。 在这⼀点上 ReadAt 不同于 Read。
若 n = len(p) 个字节在输⼊源的的结尾处由 ReadAt 返回,那么这时 err == EOF 或者 err == nil。
若 ReadAt 按查找偏移量从输⼊源读取,ReadAt 应当既不影响基本查找偏移量 也不被它所影响。
ReadAt 的客户端可对相同的输⼊源并⾏执⾏ ReadAt 调⽤。
第二个接口定义如下:
type WriterAt interface { WriteAt(p []byte, off int64) (n int, err error) }
官⽅⽂档中关于该接⼝⽅法的说明:
WriteAt 从 p 中将 len(p) 个字节写⼊到偏移量 off 处的基本数据流中。它返回从 p 中被写⼊的字节数 n(0 <= n <= len(p))以及任何遇到的引起写⼊提前停⽌ 的错误。若 WriteAt 返回的 n < len(p),它就必须返回⼀个 ⾮nil 的错误。
若 WriteAt 按查找偏移量写⼊到⽬标中,WriteAt 应当既不影响基本查找偏移量 也不被它所影响。
若区域没有重叠,WriteAt 的客户端可对相同的⽬标并⾏执⾏ WriteAt 调⽤。 我们可以通过该接⼝将数据写⼊数据流的特定偏移量之后。
ReaderFrom 和WriterTo 接⼝
这两个接口不再用传递进去的byte切片进行io操作,而是在传递进去的接口类型 参数和调用者之间直接进行数据流操作。例如:
reader := bytes.NewReader([]byte("Go语⾔中⽂⽹")) reader.WriteTo(os.Stdout)
ReaderFrom 的定义如下:
type ReaderFrom interface { ReadFrom(r Reader) (n int64, err error) }
官⽅⽂档中关于该接⼝⽅法的说明:
ReadFrom 从 r 中读取数据,直到 EOF 或发⽣错误。其返回值 n 为读取的字节 数。除 io.EOF 之外,在读取过程中遇到的任何错误也将被返回。 如果 ReaderFrom 可⽤,Copy 函数就会使⽤它。
注意:ReadFrom ⽅法不会返回 err == EOF。
WriterTo的定义如下:
type WriterTo interface { WriteTo(w Writer) (n int64, err error) }
官⽅⽂档中关于该接⼝⽅法的说明:
WriteTo 将数据写⼊ w 中,直到没有数据可写或发⽣错误。其返回值 n 为写⼊ 的字节数。 在写⼊过程中遇到的任何错误也将被返回。 如果 WriterTo 可⽤,Copy 函数就会使⽤它。
Seeker 接口
Seek是用来控制读写数据流的位置的。
接⼝定义如下:
type Seeker interface { Seek(offset int64, whence int) (ret int64, err error) }
官⽅⽂档中关于该接⼝⽅法的说明:
Seek 设置下⼀次 Read 或 Write 的偏移量为 offset,它的解释取决于 whence: 0 表示相对于⽂件的起始处,1 表示相对于当前的偏移,⽽ 2 表示相 对于其结尾处。 Seek 返回新的偏移量和⼀个错误,如果有的话。
需要注意的是,调用时whence不应该直接写0,1,2,而是应该使用在 os 包中定义的 相应的常量,以提高程序可读性。
const ( SEEK_SET int = 0 // seek relative to the origin of the file SEEK_CUR int = 1 // seek relative to the current offset SEEK_END int = 2 // seek relative to the end )
Closer 接口
接⼝定义如下:
type Closer interface { Close() error }
该接⼝⽐较简单,只有⼀个 Close() ⽅法,⽤于关闭数据流。 ⽂件 (os.File)、归档(压缩包)、数据库连接、Socket 等需要⼿动关闭的资源都实现 了 Closer 接⼝。实际编程中,经常将 Close ⽅法的调⽤放在 defer 语句中。
注意,初学者容易写出这样的代码:
file, err := os.Open("studygolang.txt") defer file.Close() if err != nil { ... }
当⽂件 studygolang.txt 不存在或找不到时,file.Close() 会panic,因为 file 是 nil。因 此,应该将 defer file.Close() 放在错误检查之后。
Copy 函数和CopyN 函数
这是非常重要的函数,通过调用上面的接口方法(Read,Write或者ReadFrom,WriteTo) 实现了从Reader类型参数向Writer类型参数传递数据流。
Copy 函数的签名:
func Copy(dst Writer, src Reader) (written int64, err error)
函数⽂档:
Copy 将 src 复制到 dst,直到在 src 上到达 EOF 或发⽣错误。它返回复制的字 节数,如果有的话,还会返回在复制时遇到的第⼀个错误。
成功的 Copy 返回 err == nil,⽽⾮ err == EOF。由于 Copy 被定义为从 src 读 取直到 EOF 为⽌,因此它不会将来⾃ Read 的 EOF 当做错误来报告。
若 dst 实现了 ReaderFrom 接⼝,其复制操作可通过调⽤ dst.ReadFrom(src) 实现。此外,若 src 实现了 WriterTo 接⼝,其复制操作可通过调⽤src.WriteTo(dst) 实现。
示例:
io.Copy(os.Stdout, strings.NewReader("Go语⾔中⽂⽹"))
CopyN 与它不同的地方在于将 n 个字节从 src 复制到 dst。签名是:
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
WriteString 函数
这是为了⽅便写⼊ string 类型提供的函数,从Writer类型参数写入基本类型string参数,函数签名:
func WriteString(w Writer, s string) (n int, err error)
当 w 实现了 WriteString ⽅法时,直接调⽤该⽅法,否则执⾏ w.Write([]byte(s))
ioutil包
util意思是工具,所以不难想到这个包是对io包进行封装后得到的操作函数集。因为这个包的函数都很简单, 所以介绍的篇幅不大。
ReadAll 函数
很多时候,我们需要⼀次性读取io.Reader中的数据。考虑到读取所有数据的需求⽐较多, Go提供了ReadAll这个函数。
func ReadAll(r io.Reader) ([]byte, error)
阅读该函数的源码发现,它是通过bytes.Buffer中的ReadFrom来实现读取所有数据的。
ReadDir 函数
在ioutil中提供了⼀个⽅便的函数:ReadDir,它读取⽬录并返回排好序的⽂件和 ⼦⽬录名(以[]os.FileInfo 的形式)。
func ReadDir(dirname string) ([]os.FileInfo, error)
返回的[]os.FileInfo是os.FileInfo的切片,每一个切片元素都是一个文件或者目录的信息。
type FileInfo interface { Name() string // base name of the file Size() int64 // length in bytes for regular files; system-dependent for others Mode() FileMode // file mode bits ModTime() time.Time // modification time IsDir() bool // abbreviation for Mode().IsDir() Sys() interface{} // underlying data source (can return nil) }
ReadFile 和 WriteFile 函数
⽅便读取文件和写入文件的函数。它们通过string参数指定文件名,操作的数据流是[]byte。
ReadFile的实现和ReadAll类似,不过,ReadFile会先判断⽂件的⼤⼩, 给bytes.Buffer⼀个预定义容量,避免额外分配内存。签名如下:
</pre>func ReadFile(filename string) ([]byte, error)</pre>
WriteFile 函数的签名如下:
func WriteFile(filename string, data []byte, perm os.FileMode) error
它将data写⼊filename⽂件中,当⽂件不存在时会创建⼀个
(⽂件权限由perm指定);
否则会先清空⽂件内容
。
TempDir 和 TempFile 函数
有时候,我们⾃⼰需要创建临时⽬录或者文件,使用:
func TempFile(dir, prefix string) (f *os.File, err error) func TempDir(dir, prefix string) (name string, err error)
第⼀个参数如果为空,表明在系统默认的临时⽬录(os.TempDir()函数可以获取到,Linux是/tmp)
中创建临时⽬录或者文件;
第⼆个参数指定临时⽬录或者文件名的前缀,该函数返回临时⽬录的路径或者临时文件的指向类型(*os.File)。
这⾥需要注意:创建者创建的临时⽂件和临时⽬录要负责删除这些临时⽬录和⽂件。 这样删除临时⽂件:
defer func() { f.Close() os.Remove(f.Name()) }()
fmt包
提供了格式化I/O函数,一般需要掌握其中的几个系列的常用函数。
Print 系列函数
这⾥说的 Print 系列函数包括:
- Fprint/Fprintf/Fprintln
- Sprint/Sprintf/Sprintln
- Print/Printf/Println
之所以将放在⼀起介绍,是因为它们的使⽤⽅式类似、参数意思也类似。
其中,Print/Printf/Println 是将内容输出到标准输出中, 会调⽤相应的F开头⼀类函数。如:
func Print(a ...interface{}) (n int, err error) { return Fprint(os.Stdout, a...) }
Fprint/Fprintf/Fprintln 函数的第⼀个参数接收⼀个io.Writer类型,会将内容输出到
io.Writer中去。
Sprint/Sprintf/Sprintln 是格式化内容为string类型,⽽并不输出到某处。
在这三组函数中, S/F/Printf 函数通过指定的格式输出或格式化内容; S/F/Print 函数
只是使⽤默认的格式输出或格式化内容; S/F/Println 函数使⽤默认的格式输出或格式
化内容,同时会在最后加上"换⾏符"
。
Print 系列函数的最后⼀个参数都是 a …interface{} 这种不定参数。对 于 S/F/Printf 序列,这个不定参数的实参个数应该和 formt 参数的占位符个数⼀致, 否则会出现格式化错误;⽽对于其他函数,当不定参数的实参个数为多个时,它们之 间会直接(对于 S/F/Print )或通过” “(空格)(对于 S/F/Println )连接起来。
注:对于 S/F/Print ,当两个参数都不是字符串时,会⾃动添加⼀个空格,否则不会加。
利⽤这⼀点,我们可以做如下事情:
result1 := fmt.Sprintln("studygolang.com", 2013) result2 := fmt.Sprint("studygolang.com", 2013)
result1的值是: studygolang.com 2013 ,result2的值是: studygolang.com2013 。
这起到了连接字符串的作⽤,⽽不需要通过strconv.Itoa()转换。
Scan 系列函数
该系列函数和 Print 系列函数相对应,包括:
- Fscan/Fscanf/Fscanln
- Sscan/Sscanf/Sscanln
- Scan/Scanf/Scanln
Fscan/Fscanf/Fscanln 函数的第⼀个参数接收⼀个io.Reader类型,从其读取内容并赋值给相应的实参。
⽽ Scan/Scanf/Scanln 正是从标准输⼊获取内容,因此,和Print/Printf/Println类似地,
直接调⽤F类函数做这件事,并将os.Stdin作为第⼀个参数传⼊。
Sscan/Sscanf/Sscanln 则直接从字符串中获取内容。
接下来重新分组进行讨论:
- Scan/FScan/Sscan 将连续由空格分隔的值存储为连续的实参(换⾏符也记为空格)
- Scanln/FScanln/Sscanln 遇到”\n”停⽌(对于Scanln,表示从标准输 ⼊获取内容,最后需要回⻋)
- Scanf/FScanf/Sscanf 将连续由空格分隔的值存储为连续的实参, 其格式由 format 决定,换⾏符处停⽌扫描
bufio包
bufio 包实现了缓存IO。该包同时为⽂本I/O提供了⼀些便利操作。带缓存的好处就在于在需要 频繁处理数据流时可以在缓存中操作,以减轻I/O和文件系统负担,同时提高操作效率。除了简单 的读写、拷贝等操作外,应该尽量使用bufio包中的接口和方法。
Reader 接口
bufio.Reader 结构包装了⼀个 io.Reader 对象,提供缓存功能,同时实现了 io.Reader 接⼝
实例化
bufio 包提供了两个实例化 bufio.Reader 对象的函数:NewReader 和NewReaderSize。 其中,NewReader 函数是调⽤ NewReaderSize 函数实现的:
func NewReader(rd io.Reader) *Reader { // 默认缓存⼤⼩:defaultBufSize=4096 return NewReaderSize(rd, defaultBufSize) }
ReadBytes 和ReadString 方法
ReadBytes⽅法签名如下:
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
ReadBytes 从输⼊中读取直到遇到界定符(delim)为⽌,返回的slice包含了从当前到界定符的内容 (包括界定符)。如果ReadBytes在遇到界定符之前就捕获到⼀个错误,它会返回 遇到错误之前已经读取的数据,和这个捕获到的错误(经常是 io.EOF)。如果ReadBytes返回的 结果line不是以界定符delim结尾,那么返回的err也⼀定不等于nil (可能是bufio.ErrBufferFull或io.EOF)。
ReadString 方法调⽤了ReadBytes⽅法,并将结果的[]byte转为string类型。
bufio.Reader 还有ReadSlice和ReadLine ⽅法。
ReadSlice返回的[]byte是指向Reader中的buffer,⽽不是copy⼀份返回。正因为ReadSlice
返回的数据会被下次的I/O操作重写,因此一般会选择使⽤ReadBytes或者ReadString来代替。
读取⼀⾏,正常⼈的思维,应该⽤ReadLine。但是它的实现,⽤不好会出现意想不到的问题,
⽐如丢数据。建议可以这么实现读取⼀⾏:
line, err := reader.ReadBytes('\n') line = bytes.TrimRight(line, "\r\n")
这样既读取了⼀⾏,也去掉了⾏尾结束符。
Writer 类型
bufio.Writer 结构包装了⼀个 io.Writer 对象,提供缓存功能,同时实现了 io.Writer 接⼝。
实例化
和 Reader 类型⼀样,bufio 包提供了两个实例化 bufio.Writer 对象的函数:NewWriter 和 NewWriterSize。其中,NewWriter 函数是调⽤ NewWriterSize 函数实现的:
func NewWriter(wr io.Writer) *Writer { // 默认缓存⼤⼩:defaultBufSize=4096 return NewWriterSize(wr, defaultBufSize) }
Flush ⽅法
该⽅法将缓存中的所有数据写⼊底层的 io.Writer 对象中。使⽤ bufio.Writer 时,在所 有的 Write 操作完成之后,应该调⽤ Flush ⽅法使得缓存都写⼊ io.Writer 对象中。
其他方法
Writer 类型其他⽅法是⼀些实际的写⽅法:
// 实现了 io.ReaderFrom 接⼝ func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) // 实现了 io.Writer 接⼝ func (b *Writer) Write(p []byte) (nn int, err error) // 实现了 io.ByteWriter 接⼝ func (b *Writer) WriteByte(c byte) error // io 中没有该⽅法的接⼝,它⽤于写⼊单个 Unicode 码点,返回写⼊的字节数(码点占⽤的字节),内 部实现会根据当前 rune 的范围调⽤ WriteByte 或 WriteString func (b *Writer) WriteRune(r rune) (size int, err error) // 写⼊字符串,如果返回写⼊的字节数⽐ len(s) ⼩,返回的error会解释原因 func (b *Writer) WriteString(s string) (int, error)
这些写⽅法在缓存满了时会调⽤ Flush ⽅法。
Scanner 类型
它是在Go1.1中增加的⼀个类型。在介绍它之前,首先需要介绍一下split、maxTokenSize 和 token,它们是Scanner 结构中的成员。
-
maxTokenSize 字段 表示通过 split 分词后的⼀个 token 允许的最⼤⻓度。在该包中 定义了⼀个常量 MaxScanTokenSize = 64 * 1024,这是允许的最⼤ token ⻓度 (64k)。
-
举例说明⼀下这⾥的 token 代表的意思: 有数据 “studygolang\tpolaris\tgolangchina”,通过”\t”进⾏分词,那么会得到三个token,它们的 内容分别是:studygolang、polaris 和 golangchina。⽽ SplitFunc 的功能是:进⾏分词,并返回 处理的数据中第⼀个 token。对于这个数据,就是返回 studygolang。
-
split 字段(SplitFunc 类型实例),代表了当前 Scanner 使⽤的分词策略, 可以使⽤下⾯介绍的预定义 SplitFunc 实例赋值,也可以⾃定义 SplitFunc 实例。(当 然,要给 split 字段赋值,必须调⽤ Scanner 的 Split ⽅法)
SplitFunc 类型定义如下: </pre> type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error) </pre> SplitFunc 定义了 ⽤于对输⼊进⾏分词的 split 函数的签名。参数 data 是还未处理的数 据,atEOF 标识 Reader 是否还有更多数据(是否到了EOF)。返回值 advance 表示 从输⼊中读取的字节数,token 表示下⼀个结果数据,err 则代表可能的错误。
在 bufio 包中预定义了⼀些 split 函数,也就是说,在 Scanner 结构中的 split 字段, 可以通过这些预定义的 split 赋值,同时 Scanner 类型的 Split ⽅法也可以接收这些预 定义函数作为参数。所以,我们可以说,这些预定义 split 函数都是 SplitFunc 类型的 实例。
这些函数包括:ScanBytes、ScanRunes、ScanWords 和 ScanLines。(由于 都是 SplitFunc 的实例,⾃然这些函数的签名都和 SplitFunc ⼀样)
- ScanBytes 返回单个字节作为⼀个 token。
- ScanRunes 返回单个 UTF-8 编码的 rune 作为⼀个 token。返回的 rune 序列 (token)和 range string类型 返回的序列是等价的。
- ScanWords 返回通过“空白符”分词的单词。注 意,这⾥的“空白符”是 unicode.IsSpace() ,即包括:’\t’, ‘\n’, ‘\v’, ‘\f’, ‘\r’, ‘ ‘, U+0085 (NEL), U+00A0 (NBSP)。
- ScanLines 返回⼀⾏⽂本,不包括⾏尾的换⾏符(包括了Windows下的”\r\n”和Unix下的”\n”)。
⼀般地,我们不会单独使⽤这些函数,⽽是提供给 Scanner 实例使⽤。
实例化
签名如下:
func NewScanner(r io.Reader) *Scanner
返回的 Scanner 实例默认的 split 函数是 ScanLines。我们实例化 Scanner 后,通过对象调⽤
Split(bufio.ScanWords) 方法可以更改 split 函数
。例如统计⼀段英⽂有多少个单词:
const input = "This is The Golang Standard Library.\nWelcome you!" scanner := bufio.NewScanner(strings.NewReader(input)) scanner.Split(bufio.ScanWords) count := 0 for scanner.Scan() { count++ } if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "reading input:", err) } fmt.Println(count)
Scan ⽅法
该⽅法好⽐ iterator 中的 Next ⽅法,它⽤于将 Scanner 获取下⼀个
token,以便 Bytes 和 Text ⽅法可⽤。
当扫描停⽌时,它返回false,这时候,要么是到了输⼊的末尾要么是遇到了⼀个错误。
Bytes 和 Text ⽅法
这两个⽅法的⾏为⼀致,都是返回最近的 token,⽆⾮ Bytes 返 回的是 []byte,Text 返回的是 string。该⽅法应该在 Scan 调⽤后调⽤,⽽且,下次调 ⽤ Scan 会覆盖这次的 token。
Err ⽅法
当 Scan 返回 false 时,通过 Err ⽅法可以获取第⼀个遇到的错误(但如果错误是 io.EOF, Err ⽅法会返回 nil)。
应用实例
对于扫描数据流分块处理,io/ioutil 包中的数据流处理方法简单单一,不能进行细节操作;fmt包中的Scan 系列函数 操作复杂,限制条件多,不容易使用。使用Scanner 类型的方法进行处理。
file, err := os.Create("scanner.txt") if err != nil { panic(err) } defer file.Close() file.WriteString("http://studygolang.com.\nIt is the home of gophers.\nIf you are stud ying golang, welcome you!") // 将⽂件 offset 设置到⽂件开头 file.Seek(0, os.SEEK_SET) scanner := bufio.NewScanner(file) for scanner.Scan() { fmt.Println(scanner.Text()) }