Go标准库常用函数与方法总结

《Go语言标准库》学习笔记

Posted by John Mactavish on September 6, 2019

前言

在开发过程中经常需要字符串处理,文件处理等操作。遇到这些问题时我经常都需要上网现查, 感觉很影响效率,也反映我对标准库的掌握还不够。所以,我今天特地又详细阅读了《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 系列函数包括:

  • 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 结构中的成员。

  1. maxTokenSize 字段 表示通过 split 分词后的⼀个 token 允许的最⼤⻓度。在该包中 定义了⼀个常量 MaxScanTokenSize = 64 * 1024,这是允许的最⼤ token ⻓度 (64k)。

  2. 举例说明⼀下这⾥的 token 代表的意思: 有数据 “studygolang\tpolaris\tgolangchina”,通过”\t”进⾏分词,那么会得到三个token,它们的 内容分别是:studygolang、polaris 和 golangchina。⽽ SplitFunc 的功能是:进⾏分词,并返回 处理的数据中第⼀个 token。对于这个数据,就是返回 studygolang。

  3. 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())
}