大白话 golang 教程-14-常用的标准库和设计
CodingByGolang
编辑于 2021年01月08日 09:15
收录于文集
共29篇

只是学习编程语言的特性做不出有用的功能,还需要访问操作系统提供的接口才行。go 语言操作系统的接口都封装到包里,典型比如的 io、os 、time 和  syscall,此外字符串处理、压缩、编码、加解密、数学、算法也基本上每个程序都会用到的功能,本章分别举例来介绍它们。

访问 https://go.dev/ 输入 io 进行搜索,发现内置的包主要是  io 和 ioutil,事实上还有一个 bufio,io 包提供了 I/O 输入输出最基本的几口,ioutil 把基本的操作封装成了易用的函数集,而 bufio 是带有缓冲特性的处理功能。

上面三个包的浏览地址:

https://pkg.go.dev/io

https://pkg.go.dev/io/ioutil

https://pkg.go.dev/bufio

要了解一个包的使用方法,最好的方式是先看它定义了那些接口,用 io 包举例:

可以发现最基本的接口是  Reader、Writer、Closer、Seeker、ReaderAt、ReaderFrom、WriterAt、WriterTo 等,其他的接口是这些接口的组合或者是针对某种类型的特殊处理,比如 ReadWriteCloser 接口,它有三个接口合并而成:

代码块
Go
自动换行
复制代码
type ReadWriteCloser interface {
  Reader
  Writer
  Closer
}
复制成功

而 ByteReader 是每次读下一个字节,它和 Reader 接口的定义区别是:

代码块
Go
自动换行
复制代码
type Reader interface {
  Read(p []byte) (n int, err error)
}

type ByteReader interface {
  ReadByte() (byte, error)
}
复制成功

所以在任何需要 io.Reader 的地方,都可以传递实现了 Read([]byte) 方法的实例,凡是需要 io.ByteReader 的地方都可以传递实现了 ReadByte() 方法的实例,定义一个测试 reader 接口的函数如下:

代码块
Go
自动换行
复制代码
func readFromReaderInstance(reader io.Reader, size int) {
  buf := make([]byte, size)
  readed, err := reader.Read(buf)

  if err != nil {
    fmt.Printf("read err: %\n", err)
  }

  fmt.Printf("%d bytes readed, content: %s\n", readed, string(buf[:readed]))
}
复制成功

os.Open 打开的文件对象 *File 实现了 io.Reader 接口,因此可以把它打开的文件对象传给 readFromReaderInstance 函数:

代码块
Go
自动换行
复制代码
fileReader, err := os.Open("/tmp/test.txt")
defer fileReader.Close() // Close 里会判断是否为 nil,不需要放在 if 后面
if err != nil {
  fmt.Printf("open file err: %s", err)
}
readFromReaderInstance(fileReader, 100)
复制成功

由于 strings.NewReader 可以从一段字符串里构造出 reader 对象,因此也很容易调用:

代码块
Go
自动换行
复制代码
strReader := strings.NewReader("i am string")
readFromReaderInstance(strReader, 100)
复制成功

甚至可以自己定义一个实现了 io.Reader 接口的对象传给它,添加如下代码:

代码块
Go
自动换行
复制代码
//MyReader 自定义实现了 Reader 接口的对象
type MyReader struct{}

func (mr *MyReader) Read(p []byte) (n int, err error) {
  copy(p, []byte("hello"))
  return 5, nil
}
复制成功

这个 MyReader 对象每次读取的时候都固定的返回 hello 文本,共 5 个字节,调用很简单:

代码块
Go
自动换行
复制代码
myReader := &MyReader{}
readFromReaderInstance(myReader, 100)
复制成功

如果从终端读取用于的输入,也能使用到 io.Reader 吗? 可以,代码如下:

代码块
Go
自动换行
复制代码
readFromReaderInstance(os.Stdin, 100)
复制成功

os.Stdin 是什么? 学习 linux 的时候还记得 0 1 2 吗? 分别对应标准输入、标准输出、错误输出,在 os.Stdin 上按 F12 查看,发现定义如下:

代码块
Go
自动换行
复制代码
// src/os/file.go
var (
  Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
  Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
  Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

// src/syscall/syscall_unix.go
var (
  Stdin  = 0
  Stdout = 1
  Stderr = 2
)
复制成功

原来是把标准的输入、输出、错误都包装了成了 *File 文件对象,难怪也支持 Read([]byte) (int, error) 读取方法,从这里也可以看出 syscall 这个包是更加底层的一个包,os 的很多易用的功能建立在 syscall 包之上。

有 Reader 就有 Writer,Writer 对函数的要求和 Reader 一样。

代码块
Go
自动换行
复制代码
type Writer interface {
  Write(p []byte) (n int, err error)
}
复制成功

经常使用的 Fprintln 和 Printf 都是对 os.Stdout 的参数调用,因为 os.Stdout 实现了 Writer 接口,它的实现如下:

代码块
Go
自动换行
复制代码
func Println(a ...interface{}) (n int, err error) {
  return Fprintln(os.Stdout, a...)
}
复制成功

常用的同时实现了 io.Reader 和 io.Writer 接口的有 os.File、bufio.Reader/Writer、bytes.Buffer、gzip.Reader/Writer、cipher.StreamReader/StreamWriter、tls.Conn、csv.Reader/Writer、net.Conn 等。

如果需要一次性的读完或者写完的操作,建议使用 ReaderFrom 和 WriterTo 接口,它们分别定义了 ReadFrom 和 WriteTo 接口,用于一次性的处理数据,ReadFrom 遇到 io.EOF 结束标志后正常停止,WriteTo 写入返回的字节数,库函数 ioutil.ReadFile 就利用了 bytes.Buffer 的 ReadFrom 来读取整个文件,它内部的 readAll 方法实现:

代码块
Go
自动换行
复制代码
// r 是 os.Open 后的 *File 对象
func readAll(r io.Reader, capacity int64) (b []byte, err error) {
  var buf bytes.Buffer
  defer func() {
    // 错误处理(略)
  }()

  if int64(int(capacity)) == capacity {
    buf.Grow(int(capacity))
  }

  _, err = buf.ReadFrom(r)
  return buf.Bytes(), err
}
复制成功

下面的例子从文件中读取内容追加到字节缓冲区里:

代码块
Go
自动换行
复制代码
func AppendFileContentTest() {
  file, err := os.Open("/tmp/test.txt")
  defer file.Close()
  if err != nil {
    fmt.Printf("open file err: %s", err)
  }

  buf := bytes.NewBufferString("hello world, from file:")
  buf.ReadFrom(file) // bytes.Buffer 实现了 ReadFrom 接口,而 *File 实现了 Reader 接口
  fmt.Println(buf.String())
}
复制成功

io.Reader 和 io.Writer 对读写进行了简洁的定义,但是在处理数据的时候,经常需要进行偏移量读取,就可以考虑 ReaderAt、WriterAt、Seeker 接口,这三个接口都可以实现偏移量的功能,不过 Seeker 接口要更加灵活一下,它定义的 Seek 方法:

代码块
Go
自动换行
复制代码
type Seeker interface {
  Seek(offset int64, whence int) (ret int64, err error)
}
复制成功

其中第二个参数 whence 表示了相对位置:

1. io.SeekStart 相对于文件的开头

2. io.SeekCurrent 相当于当前的偏移位置

3. io.SeekEnd 相对于文件结束

下面的示例代码分别从尾部读取和从开头,都是读取的中间那个汉字:

代码块
Go
自动换行
复制代码
func MoveSeekPosTest() {
  reader := strings.NewReader("中国人第一")
  reader.Seek(-9, io.SeekEnd) // 注意 UTF-8 编码,相当于 3*X 个字节移动
  r, _, _ := reader.ReadRune()
  fmt.Printf("%c\n", r) // 人
  reader.Seek(6, io.SeekStart)
  r, _, _ = reader.ReadRune()
  fmt.Printf("%c\n", r) // 人
}
复制成功

和这些接口比较,Closer 接口最简单,用于关闭数据流,只有一个 Close 方法,经常用在 defer 语句里,当要关闭的资源是 nil 的时候,Close 函数可能会返回 error 对象 ErrInvalid,所以调用的时候不用担心 file 是否为 nil,把 defer 直接写在后面会显得逻辑更紧凑一些,下面第一种写法更好。

代码块
Go
自动换行
复制代码
// 第一种
file, err := os.Open("/tmp/test.txt")
defer file.Close()
if err != nil {
  // 打开文件失败了
}


// 第二种
file, err := os.Open("/tmp/test.txt")
if err != nil {
  // 打开文件失败了
}
defer file.Close()
复制成功

ByteReader 和 ByteWriter 每次读写只能是一个字节,在压缩和数据包协议里会用的比较多。

而 ByteScanner 和 RuneScanner 接口就比较有意思,它们都内部含有一个 Reader 接口,比如 ByteScanner 的定义如下:

代码块
Go
自动换行
复制代码
type ByteScanner interface {
  ByteReader
  UnreadByte() error
}
复制成功

也就是比 ByteReader 多了一个 UnreadByte 函数,它的作用是把上一次 ReadByte 读取的字节再放回去,再次调用 ReadByte 和上一次的结果是一样的,所以它的规则就是调用了 ReadByte 之后才能用,而且不能连续的调用 UnreadByte,RuneReader 和 RuneScanner 是类似的作用,但它作用于 Unicode 字符。这种回退机制有时候在解析数据包协议的时候特别有用。

此外 io 包还定义了一些结构体类型:

1. SectionReader 内嵌 ReaderAt 接口,可以设置便宜后读取指定的字节数

2. LimitedReader 内嵌了 Reader 接口,每次读了后都更新一下剩余的可读字节数

3. PipeReader 和 PipeWriter 管道读写

几个重要的函数:

1. Copy 和 CopyN,拷贝数据,从 Reader 到 Writer

2. ReadAtLeast 至少读多少个字节,ReadFull 将传递的 buf 读满

3. WriteString 对 []byte 的包装,等同于操作 []byte(string) 参数

4. MultiReader 和 MultiWriter 在逻辑上合并多个 Reader/Writer

5. TeeReader 和 tee 命令类似,把 Reader 中的内容自动写入到 Writer 中

bufio 对 io.Reader 和 io.Writer 进行了包装,提供了带缓存的实现,可以由 NewReader、NewReaderSize 构建,NewReader 函数以 4096 的默认换冲区大小来调用 NewReaderSize 实现,提供了 ReadSlice、ReadByte(s)、ReadString、ReadLine、ReadRune、Peek、Reset、UnreadByte、UnreadRune、WriteTo 等方法。bufio 还提供了 Scanner 专门处理一行、分隔的输入问题,它使用内部的 split 函数辅助分割 token 标识符,split 函数又是一个 SplitFunc 对象:

代码块
Go
自动换行
复制代码
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
复制成功

该函数暴露到包外,可以独立使用,在使用 Scanner 的 Scan 函数之前,一定要调用 Split 函数,它的作用是指明要分割的规则,比如 bufio.ScanWords 按单词分割、bufio.ScanLines 按行分割等。

代码块
Go
自动换行
复制代码
func CountWordsTest(input string) {
  scanner := bufio.NewScanner(strings.NewReader(input))
  scanner.Split(bufio.ScanWords)
  total := 0

  // 返回 false 时停止扫描,可能扫描完了,也可能是出错了
  // 需要判断 Err() 函数的值来确定
  for scanner.Scan() {
    total++
  }

  // 不会是 io.EOF 错误
  if err := scanner.Err(); err != nil {
    fmt.Println("出错了:" + err.Error())
  }

  fmt.Printf("包含 %d 个单词\n", total)
}
复制成功

os 包的作用是封装一些跨平台的功能,它依赖于 syscall 包,不过 os 能完成的时候尽量不要去调用 syscall 包,处理文件系统、权限、用户等,有时候需要 path 包来协助,path 包对不同的系统的路径处理提供了有用的函数,比如 windows 以 \ 分割路径,而 *nix 系统以 / 分割路径,还有相对路径、..、盘符等,如果自行拼接路径可能造成程序兼容性差,在 mac 上运行好好的,放到 windows 就异常了。

日期时间处理由 time 包完成,time 包很重要,基本上每个程序都会用,它的主要类型有:

1. Location 时区

2. Time 时间点

3. Duration 时间段,以纳秒为单位

4. Timer、Ticker 计时器

直接调用 time.Now() 即可获得当前时间,两个时间差就是 time.Duration 对象,直接打印 Duration 对象会输出可阅读的时分秒,因为 Duration 实现了 fmt.Stringer 接口:

代码块
Go
自动换行
复制代码
t1, _ := time.Parse("2006-01-02 15:04:05", "2020-12-31 23:59:59")
d1 := now.Sub(t1)
fmt.Println(d1) // 125h50m8.589151s
复制成功

Duration 的定义其实是一个 64 位的整数:

代码块
Go
自动换行
复制代码
type Duration int64
复制成功

对日期的增减通过 Sub、Add、AddDate  实现,用 Before、After 判断大小,增加 1 年的时间:

代码块
Go
自动换行
复制代码
now := time.Now()
fmt.Println(now.AddDate(1, 0, 0))
fmt.Println(now.Add(time.Duration(365 * 24 * 60 * 60 * 1000000000)))
复制成功

需要返回整点、整分除了可以使用 Format("2006-01-02 12:00:00&#​34;) 这种形式也可以直接调用函数 Round、Truncate,经常调用的还有 time.Sleep 函数,它会挂起当前 goroutine 到指定的时间,计时器 Timer、Ticker 和通道 chan 有关,简单的理解就是用于消息通讯,<- signal 等待 signal 有消息取出来,关键字 go func() {...} () 表示开启一个协程 goroutine 让 go 运行时进行调度,注意的是对 ticker 对象需调用 Stop 来释放资源,想一直运行可以直接使用 time.Tick 来得到计时器,不过它只是 NewTicker 函数的简单包装,会一直运行下去直到进程退出。有关通道的知识下一章详细学习。

代码块
Go
自动换行
复制代码
//TimerAfterTest 计时器
func TimerAfterTest() {
  signal := time.After(1 * time.Second)
  select {
  case <-signal:
    fmt.Println("到时间了...")
  case <-time.After(2 * time.Second):
    fmt.Println("超过2秒了...")
  }

  time.AfterFunc(1*time.Second, func() {
    fmt.Println("我1秒后打印")
  })
}


// TickerTest 定时器
func TickerTest() {
  ticker := time.NewTicker(1 * time.Second)
  done := make(chan bool)

  go func() {
    time.Sleep(3 * time.Second)
    done <- true
  }()

  for {
    select {
    case <-done:
      ticker.Stop()
      return
    case <-ticker.C:
      fmt.Println("时间它滴答滴答滴")
    }
  }
}
复制成功

处理文本的包主要由 strings、bytes、strconv、regexp、unicode 几个包完成,默认的 strings.Index 返回的是 ASCII 编码的索引,UTF-8 的版本需要借助于 utf8.RuneCountInString 函数实现:

代码块
Go
自动换行
复制代码
func Utf8Index(str, substr string) int {
  index := strings.Index(str, substr)
  if index < 0 {
    return -1
  }
  return utf8.RuneCountInString(str[:index])
}

fmt.Println(strings.Index("都是NO.1中国人", "中国"))    // 10
fmt.Println(strlib.Utf8Index("都是NO.1中国人", "中国")) // 6
复制成功

此外还有 encoding、compress、archive、math、crypto、sort 等,需要使用的时候去 go.dev 查询包的文档,下面是计算 md5 编码的例子:

代码块
Go
自动换行
复制代码
func GetStrMD5(str string) string {
  r := md5.Sum([]byte(str))
  return hex.EncodeToString(r[:])
}
复制成功

自定义排序需要实现 sort.Interface 接口,它有三个方法:

1. len 长度

2. swap 交换 2 个值

3. less 比大小

比如实现按照年龄排序:

代码块
Go
自动换行
复制代码
type PersonSlice []*Person

func (ps PersonSlice) Len() int {
  return len(ps)
}

func (ps PersonSlice) Swap(i, j int) {
  ps[i], ps[j] = ps[j], ps[i]
}

func (ps PersonSlice) Less(i, j int) bool {
  // 顺序
  return ps[j].Age > ps[i].Age
}
复制成功

测试代码:

代码块
Go
自动换行
复制代码
func SortPersonTest() {
  persons := PersonSlice{
    &Person{"zhangsan", 27},
    &Person{"lisi", 22},
    &Person{"wangwu", 38},
  }

  sort.Sort(persons)
  for _, person := range persons {
    fmt.Println(*person)
  }
}
复制成功

包里很多功能还没有介绍,经常浏览 go 的官方包和源代码,会有意想不到的收获。

本章节的代码 https://github.com/developdeveloper/go-demo/tree/master/14-about-std-lib