只是学习编程语言的特性做不出有用的功能,还需要访问操作系统提供的接口才行。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 接口,它有三个接口合并而成:
type ReadWriteCloser interface {
Reader
Writer
Closer
} 而 ByteReader 是每次读下一个字节,它和 Reader 接口的定义区别是:
type Reader interface {
Read(p []byte) (n int, err error)
}
type ByteReader interface {
ReadByte() (byte, error)
}
所以在任何需要 io.Reader 的地方,都可以传递实现了 Read([]byte) 方法的实例,凡是需要 io.ByteReader 的地方都可以传递实现了 ReadByte() 方法的实例,定义一个测试 reader 接口的函数如下:
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 函数:
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 对象,因此也很容易调用:
strReader := strings.NewReader("i am string")
readFromReaderInstance(strReader, 100) 甚至可以自己定义一个实现了 io.Reader 接口的对象传给它,添加如下代码:
//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 个字节,调用很简单:
myReader := &MyReader{}
readFromReaderInstance(myReader, 100) 如果从终端读取用于的输入,也能使用到 io.Reader 吗? 可以,代码如下:
readFromReaderInstance(os.Stdin, 100) os.Stdin 是什么? 学习 linux 的时候还记得 0 1 2 吗? 分别对应标准输入、标准输出、错误输出,在 os.Stdin 上按 F12 查看,发现定义如下:
// 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 一样。
type Writer interface {
Write(p []byte) (n int, err error)
} 经常使用的 Fprintln 和 Printf 都是对 os.Stdout 的参数调用,因为 os.Stdout 实现了 Writer 接口,它的实现如下:
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 方法实现:
// 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
} 下面的例子从文件中读取内容追加到字节缓冲区里:
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 方法:
type Seeker interface {
Seek(offset int64, whence int) (ret int64, err error)
} 其中第二个参数 whence 表示了相对位置:
1. io.SeekStart 相对于文件的开头
2. io.SeekCurrent 相当于当前的偏移位置
3. io.SeekEnd 相对于文件结束
下面的示例代码分别从尾部读取和从开头,都是读取的中间那个汉字:
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 直接写在后面会显得逻辑更紧凑一些,下面第一种写法更好。
// 第一种
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 的定义如下:
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 对象:
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error) 该函数暴露到包外,可以独立使用,在使用 Scanner 的 Scan 函数之前,一定要调用 Split 函数,它的作用是指明要分割的规则,比如 bufio.ScanWords 按单词分割、bufio.ScanLines 按行分割等。
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 接口:
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 位的整数:
type Duration int64 对日期的增减通过 Sub、Add、AddDate 实现,用 Before、After 判断大小,增加 1 年的时间:
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") 这种形式也可以直接调用函数 Round、Truncate,经常调用的还有 time.Sleep 函数,它会挂起当前 goroutine 到指定的时间,计时器 Timer、Ticker 和通道 chan 有关,简单的理解就是用于消息通讯,<- signal 等待 signal 有消息取出来,关键字 go func() {...} () 表示开启一个协程 goroutine 让 go 运行时进行调度,注意的是对 ticker 对象需调用 Stop 来释放资源,想一直运行可以直接使用 time.Tick 来得到计时器,不过它只是 NewTicker 函数的简单包装,会一直运行下去直到进程退出。有关通道的知识下一章详细学习。
//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 函数实现:
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 编码的例子:
func GetStrMD5(str string) string {
r := md5.Sum([]byte(str))
return hex.EncodeToString(r[:])
}
自定义排序需要实现 sort.Interface 接口,它有三个方法:
1. len 长度
2. swap 交换 2 个值
3. less 比大小
比如实现按照年龄排序:
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
} 测试代码:
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