标准库
文档与源代码
目前,标准库里总共有超过100 个包,这些包被分到 38 个类别里。
archive bufio bytes compress container crypto database
debug encoding errors expvar flag fmt go
hash html image index io log math
mime net os path reflect regexp runtime
sort strconv strings sync syscall testing text
time unicode unsafe
如果想了解所有包以及更详细的描述,Go 语言团队在网站上维护了一个文档,参见 http://golang.org/pkg/
不管用什么方式安装 Go,标准库的源代码都会安装在$GOROOT/src/pkg 文件夹中。拥有标 准库的源代码对 Go 工具正常工作非常重要。类似 godoc、gocode 甚至 go build 这些工具, 都需要读取标准库的源代码才能完成其工作。如果源代码没有安装在以上文件夹中,或者无法通 过$GOROOT 变量访问,在试图编译程序时会产生错误。作为 Go 发布包的一部分,标准库的源代码是经过预编译的。这些预编译后的文件,称作归档文件(archive file),可以 在$GOROOT/pkg 文件夹中找到已经安装的各目标平台和操作系统的归档文件。
这些文件是特殊的 Go 静态库文件,由 Go 的构建工具创建,并在编译和链接最终程序时被 使用。归档文件可以让构建的速度更快。但是在构建的过程中,没办法指定这些文件,所以没办 法与别人共享这些文件。Go 工具链知道什么时候可以使用已有的.a 文件,什么时候需要从机器 上的源代码重新构建。
记录日志
日志是开发人员的眼睛和耳朵,可以用来跟踪、调试和分析代码。基于此,标准库提供了 log 包,可以对日志做一些最基本的配置。根据特殊需要,开发人员还可以自己定制日志记录器。
在 UNIX 里,日志有很长的历史。这些积累下来的经验都体现在 log 包的设计里。传统的 CLI(命令行界面)程序直接将输出写到名为 stdout 的设备上。所有的操作系统上都有这种设 备,这种设备的默认目的地是标准文本输出。默认设置下,终端会显示这些写到 stdout 设备上 的文本。这种单个目的地的输出用起来很方便,不过你总会碰到需要同时输出程序信息和输出执 行细节的情况。这些执行细节被称作日志。当想要记录日志时,你希望能写到不同的目的地,这 样就不会将程序的输出和日志混在一起了。
为了解决这个问题,UNIX 架构上增加了一个叫作 stderr 的设备。这个设备被创建为日志 的默认目的地。这样开发人员就能将程序的输出和日志分离开来。如果想在程序运行时同时看到 程序输出和日志,可以将终端配置为同时显示写到 stdout 和 stderr 的信息。不过,如果用 户的程序只记录日志,没有程序输出,更常用的方式是将一般的日志信息写到 stdout,将错误 或者警告信息写到 stderr。
log 包
记录日志的目的是跟踪程序什么时候在什么位置做了什么。这就需要通过某些配置在每个日志项上要写的一些信息,下面是跟踪日志的样例:
TRACE: 2009/11/10 23:00:00.000000 /tmpfs/gosandbox-/prog.go:14: message
可以看到一个由 log 包产生的日志项。这个日志项包含前缀、日期时间戳、该日志具体是由哪个源文件记录的、源文件记录日志所在行,最后是日志消息。让我们看一下如何配置 log 包来输出这样的日志项:
// 这个示例程序展示如何使用最基本的 log 包
package main
import (
"log"
)
func init() {
log.SetPrefix("TRACE: ")
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}
func main() {
// Println 写到标准日志记录器
log.Println("message")
// Fatalln 在调用 Println()之后会接着调用 os.Exit(1)
log.Fatalln("fatal message")
// Panicln 在调用 Println()之后会接着调用 panic()
log.Panicln("panic message")
}
如果执行代码中的程序,输出的结果会跟踪日志样例所示的输出类似。其中有几个和 log 包相关联的标志,这些标志用来控制可以写到每个日志项的其他信息。
const (
// 将下面的位使用或运算符连接在一起,可以控制要输出的信息。没有
// 办法控制这些信息出现的顺序(下面会给出顺序)或者打印的格式
// (格式在注释里描述)。这些项后面会有一个冒号:
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
// 日期: 2009/01/23
Ldate = 1 << iota
// 时间: 01:23:23
Ltime
// 毫秒级时间: 01:23:23.123123。该设置会覆盖 Ltime 标志
Lmicroseconds
// 完整路径的文件名和行号: /a/b/c/d.go:23
Llongfile
// 最终的文件名元素和行号: d.go:23
// 覆盖 Llongfile
Lshortfile
// 标准日志记录器的初始值
LstdFlags = Ldate | Ltime
)
声明 Ldate 常量 关键字 iota 在常量声明区里有特殊的作用。这个关键字让编译器为每个常量复制相同的表 达式,直到声明区结束,或者遇到一个新的赋值语句。关键字 iota 的另一个功能是,iota 的 初始值为 0,之后 iota 的值在每次处理为常量后,都会自增 1。
const (
Ldate = 1 << iota // 1 << 0 = 000000001 = 1
Ltime // 1 << 1 = 000000010 = 2
Lmicroseconds // 1 << 2 = 000000100 = 4
Llongfile // 1 << 3 = 000001000 = 8
Lshortfile // 1 << 4 = 000010000 = 16
...
)
操作符<<对左边的操作数执行按位左移操作。在每个常量声明时,都将 1 按位左移 iota 个位置。最终的效果使为每个常量赋予一个独立位置的位,这正好是标志希望的工作方式。
声明 LstdFlags 常量
const (
...
LstdFlags = Ldate(1) | Ltime(2) = 00000011 = 3
)
因为使用了复制操作符,LstdFlags 打破了 iota 常数链。由于 有|运算符用于执行或操作,常量 LstdFlags 被赋值为 3。对位进行或操作等同于将每个位置 的位组合在一起,作为最终的值。如果对位 1 和 2 进行或操作,最终的结果就是 3。
func init() {
...
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}
这里我们将 Ldate、Lmicroseconds 和 Llongfile 标志组合在一起,将该操作的值传入 SetFlags 函数。这些标志值组合在一起后,最终的值是 13,代表第 1、3 和 4 位为 1(00001101)。由于每个常量表示单独一个位,这些标志经过或操作组合后的值,可以表示每个需要的日志参数。之后 log 包会按位检查这个传入的整数值,按照需求设置日志项记录的信息。
log 包有一个很方便的地方就是,这些日志记录器是多 goroutine 安全的。这意味着在多个 goroutine 可以同时调用来自同一个日志记录器的这些函数,而不 会有彼此间的写冲突。标准日志 记录器具有这一性质,用户定制的日志记录器也应该满足这一性质。
定制的日志记录器
要想创建一个定制的日志记录器,需要创建一个 Logger 类型值。可以给每个日志记录器配 置一个单独的目的地,并独立设置其前缀和标志。
// 这个示例程序展示如何创建定制的日志记录器
package main
import (
"io"
"io/ioutil"
"log"
"os"
)
var (
Trace *log.Logger // 记录所有日志
Info *log.Logger // 重要的信息
Warning *log.Logger // 需要注意的信息
Error *log.Logger // 非常严重的问题
)
func init() {
file, err := os.OpenFile("errors.txt",
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open error log file:", err)
}
Trace = log.New(ioutil.Discard,
"TRACE: ",
log.Ldate|log.Ltime|log.Lshortfile)
Info = log.New(os.Stdout,
"INFO: ",
log.Ldate|log.Ltime|log.Lshortfile)
Warning = log.New(os.Stdout,
"WARNING: ",
log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(io.MultiWriter(file, os.Stderr),
"ERROR: ",
log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
Trace.Println("I have something standard to say")
Info.Println("Special Information")
Warning.Println("There is something you need to know about")
Error.Println("Something has failed")
}
这段程序创建了 4 种不同的 Logger 类型的指针变量,分别命名为 Trace、Info、Warning 和 Error。每个变量使用不同的配置,用来表示不同的重要程度。为了创建每个日志记录器,我们使用了 log 包的 New 函数,它创建并正确初始化一个Logger 类型的值。函数 New 会返回新创建的值的地址。在 New 函数创建对应值的时候,我们需要给它传入一些参数
// New 创建一个新的 Logger。out 参数设置日志数据将被写入的目的地
// 参数 prefix 会在生成的每行日志的最开始出现
// 参数 flag 定义日志记录包含哪些属性
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
第一个参数 out 指定了日志要写到的目的地。这个参数传入的值必须实现了 io.Writer 接口。第二个参数 prefix 是之前看到的前缀,而日志的标志则是最后一个参数。在这个程序里,Trace 日志记录器使用了 ioutil 包里的 Discard 变量作为写到的目的地,变量 Discard 有一些有意思的属性。
// devNull 是一个用 int 作为基础类型的类型
type devNull int
// Discard 是一个 io.Writer,所有的 Write 调用都不会有动作,但是会成功返回
var Discard io.Writer = devNull(0)
// io.Writer 接口的实现
func (devNull) Write(p []byte) (int, error) {
return len(p), nil
}
Discard 变量的类型被声明为 io.Writer 接口类型,并被给定了一个 devNull 类型的值 0。基于 devNull 类型实现的 Write 方法,会忽略所有写入这一变量的数据。当某个等级的日志不重要时,使用 Discard 变量可以禁用这个等级的日志。日志记录器 Info 和 Warning 都使用 stdout 作为日志输出,变量 Stdout 的声明也有一些有意思的地方。
// Stdin、Stdout 和 Stderr 是已经打开的文件,分别指向标准输入、标准输出和
// 标准错误的文件描述符
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
os/file_unix.go
// NewFile 用给出的文件描述符和名字返回一个新 File
func NewFile(fd uintptr, name string) *File
可以看到 3 个变量的声明,分别表示所有操作系统里都有的 3 个标准输入/输出,即 Stdin、Stdout 和 Stderr。这 3 个变量都被声明为 File 类型的指针,这个类型实现了 io.Writer 接口。有了这个知识,我们来看一下最后的日志记录器 Error,Error 使用了io 包里的 MultiWriter 函数,这个函数调用会返回一个 io.Writer接口类型值,这个值包含之前打开的文件 file,以及 stderr。MultiWriter 函数是一个变参函数,可以接受任意个实现了 io.Writer 接口的值。这个函数会返回一个 io.Writer 值,这个值会把所有传入的 io.Writer 的值绑在一起。当对这个返回值进行写入时,会向所有绑在一起的io.Writer 值做写入。这让类似 log.New 这样的函数可以同时向多个 Writer 做输出。现在,当我们使用 Error 记录器记录日志时,输出会同时写到文件和 stderr。
func main() {
Trace.Println("I have something standard to say")
Info.Println("Special Information")
Warning.Println("There is something you need to know about")
Error.Println("Something has failed")
}
我们用自己创建的每个记录器写一条消息。每个记录器变量都包含一组方法,这组方法与 log 包里实现的 那组函数完全一致.
不同的日志方法的声明
func (l *Logger) Fatal(v ...interface{})
func (l *Logger) Fatalf(format string, v ...interface{})
func (l *Logger) Fatalln(v ...interface{})
func (l *Logger) Flags() int
func (l *Logger) Output(calldepth int, s string) error
func (l *Logger) Panic(v ...interface{})
func (l *Logger) Panicf(format string, v ...interface{})
func (l *Logger) Panicln(v ...interface{})
func (l *Logger) Prefix() string
func (l *Logger) Print(v ...interface{})
func (l *Logger) Printf(format string, v ...interface{})
func (l *Logger) Println(v ...interface{})
func (l *Logger) SetFlags(flag int)
func (l *Logger) SetPrefix(prefix string)
log 包的实现,是基于对记录日志这个需求长时间的实践和积累而形成的。将输出写到stdout,将日志记录到 stderr,是很多基于命令行界面(CLI)的程序的惯常使用的方法。不过如果你的程序只输出日志,那么使用 stdout、stderr 和文件来记录日志是很好的做法。 标准库的 log 包包含了记录日志需要的所有功能,推荐使用这个包。我们可以完全信任这个包的实现,不仅仅是因为它是标准库的一部分,而且社区也广泛使用它。
编码/解码
许多程序都需要处理或者发布数据,不管这个程序是要使用数据库,进行网络调用,还是与 分布式系统打交道。如果程序需要处理 XML 或者 JSON,可以使用标准库里名为 xml 和 json 的包,它们可以处理这些格式的数据。如果想实现自己的数据格式的编解码,可以将这些包的实 现作为指导。
解码 JSON
如果要处理来自网络响应或者文件的 JSON,那么一定会用到 json 包的 NewDecoder 函数以及 Decode 方法进行解码,让我们来看一个处理 Get 请求响应的 JSON 的例子。
Google 搜索 API 的 JSON 响应例子
{
"responseData": {
"results": [
{
"GsearchResultClass": "GwebSearch",
"unescapedUrl": "https://www.reddit.com/r/golang",
"url": "https://www.reddit.com/r/golang",
"visibleUrl": "www.reddit.com",
"cacheUrl": "http://www.google.com/search?q=cache:W...",
"title": "r/\u003cb\u003eGolang\u003c/b\u003e - Reddit",
"titleNoFormatting": "r/Golang - Reddit",
"content": "First Open Source \u003cb\u003eGolang\u..."
},
{
"GsearchResultClass": "GwebSearch",
"unescapedUrl": "http://tour.golang.org/",
"url": "http://tour.golang.org/",
"visibleUrl": "tour.golang.org",
"cacheUrl": "http://www.google.com/search?q=cache:O...",
"title": "A Tour of Go",
"titleNoFormatting": "A Tour of Go",
"content": "Welcome to a tour of the Go programming ..."
}
]
}
}
使用 json 包和 NewDecoder 函数
// 这个示例程序展示如何使用 json 包和 NewDecoder 函数
// 来解码 JSON 响应
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type (
// gResult 映射到从搜索拿到的结果文档
gResult struct {
GsearchResultClass string `json:"GsearchResultClass"`
UnescapedURL string `json:"unescapedUrl"`
URL string `json:"url"`
VisibleURL string `json:"visibleUrl"`
CacheURL string `json:"cacheUrl"`
Title string `json:"title"`
TitleNoFormatting string `json:"titleNoFormatting"`
Content string `json:"content"`
}
// gResponse 包含顶级的文档
gResponse struct {
ResponseData struct {
Results []gResult `json:"results"`
} `json:"responseData"`
}
)
func main() {
uri := "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&rsz=8&q=golang"
// 向 Google 发起搜索
resp, err := http.Get(uri)
if err != nil {
log.Println("ERROR:", err)
return
}
defer resp.Body.Close()
// 将 JSON 响应解码到结构类型
var gr gResponse
err = json.NewDecoder(resp.Body).Decode(&gr)
if err != nil {
log.Println("ERROR:", err)
return
}
fmt.Println(gr)
}
gResponse 和 gResult 的类型声明,你会注意到每个字段最后使用单引号声明了一个字符串。这些字符串被称作标签(tag),是提供每个字段的元信息的一种机制,将 JSON 文档和结构类型里的字段一一映射起来。如果不存在标签,编码和解码过程会试图以大小写无关的方式,直接使用字段的名字进行匹配。如果无法匹配,对应的结构类型的字段就包含其零值。下面让我们看一下标准库里 NewDecoder 函数和 Decode 方法的声明
// NewDecoder 返回从 r 读取的解码器
//
// 解码器自己会进行缓冲,而且可能会从 r 读比解码 JSON 值
// 所需的更多的数据
func NewDecoder(r io.Reader) *Decoder
// Decode 从自己的输入里读取下一个编码好的 JSON 值,
// 并存入 v 所指向的值里
//
// 要知道从 JSON 转换为 Go 的值的细节,
// 请查看 Unmarshal 的文档
func (dec *Decoder) Decode(v interface{}) error
可以看到 NewDecoder 函数接受一个实现了 io.Reader 接口类型的值作为参数。函数 NewDecoder 返回一个指向 Decoder 类型的指针值。由于 Go 语言支持复合语句调用,可以直接调用从 NewDecoder 函数返回的值的 Decode 方法,而不用把这个返回值存入变量。使用反射,Decode 方法会拿到传入值的类型信息。然后,在读取 JSON响应的过程中,Decode 方法会将对应的响应解码为这个类型的值。这意味着用户不需要创建对应的值,Decode 会为用户做这件事情。
有时,需要处理的 JSON 文档会以 string 的形式存在。在这种情况下,需要将 string 转换为 byte 切片([]byte),并使用 json 包的 Unmarshal 函数进行反序列化的处理。
// 这个示例程序展示如何解码 JSON 字符串
package main
import (
"encoding/json"
"fmt"
"log"
)
// Contact 结构代表我们的 JSON 字符串
type Contact struct {
Name string `json:"name"`
Title string `json:"title"`
Contact struct {
Home string `json:"home"`
Cell string `json:"cell"`
} `json:"contact"`
}
// JSON 包含用于反序列化的演示字符串
var JSON = `{
"name": "Gopher",
"title": "programmer",
"contact": {
"home": "415.333.3333",
"cell": "415.555.5555"
}
}`
func main() {
// 将 JSON 字符串反序列化到变量
var c Contact
err := json.Unmarshal([]byte(JSON), &c)
if err != nil {
log.Println("ERROR:", err)
return
}
fmt.Println(c)
}
我们的例子将 JSON 文档保存在一个字符串变量里,并使用 Unmarshal 函数将 JSON 文档解码到一个结构类型的值里。有时,无法为 JSON 的格式声明一个结构类型,而是需要更加灵活的方式来处理 JSON 文档。在这种情况下,可以将 JSON 文档解码到一个 map 变量中
// 这个示例程序展示如何解码 JSON 字符串
package main
import (
"encoding/json"
"fmt"
"log"
)
// JSON 包含要反序列化的样例字符串
var JSON = `{
"name": "Gopher",
"title": "programmer",
"contact": {
"home": "415.333.3333",
"cell": "415.555.5555"
}
}`
func main() {
// 将 JSON 字符串反序列化到 map 变量
var c map[string]interface{}
err := json.Unmarshal([]byte(JSON), &c)
if err != nil {
log.Println("ERROR:", err)
return
}
fmt.Println("Name:", c["name"])
fmt.Println("Title:", c["title"])
fmt.Println("Contact")
fmt.Println("H:", c["contact"].(map[string]interface{})["home"])
fmt.Println("C:", c["contact"].(map[string]interface{})["cell"])
}
变量 c 声明为一个 map 类型,其键是 string 类型,其值是 interface{}类型。这意味着这个 map 类型可以使用任意类型的值作为给定键的值。虽然这种方法为处理 JSON 文档带来了很大的灵活性,但是却有一个小缺点。因为每个键的值的类型都是 interface{},所以必须将值转换为合适的类型,才能处理这个值。
编码 JSON
我们要学习的处理 JSON 的第二个方面是,使用 json 包的 MarshalIndent 函数进行编码。这个函数可以很方便地将 Go 语言的 map 类型的值或者结构类型的值转换为易读格式的 JSON 文档。序列化(marshal)是指将数据转换为 JSON 字符串的过程。下面是一个将 map 类型转换为 JSON字符串的例子
// 这个示例程序展示如何序列化 JSON 字符串
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
// 创建一个保存键值对的映射
c := make(map[string]interface{})
c["name"] = "Gopher"
c["title"] = "programmer"
c["contact"] = map[string]interface{}{
"home": "415.333.3333",
"cell": "415.555.5555",
}
// 将这个映射序列化到 JSON 字符串
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
log.Println("ERROR:", err)
return
}
fmt.Println(string(data))
}
函数 MarshalIndent 返回一个 byte 切片,用来保存 JSON 字符串和一个 error 值。
// MarshalIndent 很像 Marshal,只是用缩进对输出进行格式化
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
如果不需要输出带有缩进格式的 JSON 字符串,json 包还提供了名为 Marshal 的函数来进行解码。这个函数产生的 JSON 字符串很适合作为在网络响应(如 Web API)的数据。函数 Marshal的工作原理和函数 MarshalIndent 一样,只不过没有用于前缀 prefix 和缩进 indent 的参数。
在标准库里都已经提供了处理 JSON 和 XML 格式所需要的诸如解码、反序列化以及序列化数据的功能。随着每次 Go 语言新版本的发布,这些包的执行速度也越来越快。这些包是处理 JSON和 XML 的最佳选择。由于有反射包和标签的支持,可以很方便地声明一个结构类型,并将其中的字段映射到需要处理和发布的文档的字段。由于 json 包和 xml 包都支持 io.Reader 和io.Writer 接口,用户不用担心自己的 JSON 和 XML 文档源于哪里。所有的这些特性都让处理 JSON 和 XML 变得很容易。
输入和输出
类 UNIX 的操作系统如此伟大的一个原因是,一个程序的输出可以是另一个程序的输入这一理念。依照这个哲学,这类操作系统创建了一系列的简单程序,每个程序只做一件事,并把这件事做得非常好。之后,将这些程序组合在一起,可以创建一些脚本做一些很惊艳的事情。这些程序使用 stdin 和 stdout 设备作为通道,在进程之间传递数据。
同样的理念扩展到了标准库的 io 包,而且提供的功能很神奇。这个包可以以流的方式高效处理数据,而不用考虑数据是什么,数据来自哪里,以及数据要发送到哪里的问题。与 stdout和 stdin 对应,这个包含有 io.Writer 和 io.Reader 两个接口。所有实现了这两个接口的类型的值,都可以使用 io 包提供的所有功能,也可以用于其他包里接受这两个接口的函数以及方法。这是用接口类型来构造函数和 API 最美妙的地方。开发人员可以基于这些现有功能进行组合,利用所有已经存在的实现,专注于解决业务问题。
Writer 和 Reader 接口
io 包是围绕着实现了 io.Writer 和 io.Reader 接口类型的值而构建的。由于 io.Writer和 io.Reader 提供了足够的抽象,这些 io 包里的函数和方法并不知道数据的类型,也不知道这些数据在物理上是如何读和写的
type Writer interface {
Write(p []byte) (n int, err error)
}
这个接口声明了唯一一个方法 Write,这个方法接受一个 byte 切片,并返回两个值。第一个值是写入的字节数,第二个值是 error 错误值。
Write 从 p 里向底层的数据流写入 len(p)字节的数据。这个方法返回从 p 里写出的字节数(0 <= n <= len(p)),以及任何可能导致写入提前结束的错误。Write 在返回 n < len(p)的时候,必须返回某个非 nil 值的 error。Write 绝不能改写切片里的数据,哪怕是临时修改也不行。
这些规则意味着 Write 方法的实现需要试图写入被传入的 byte 切片里的所有数据。但是,如果无法全部写入,那么该方法就一定会返回一个错误。返回的写入字节数可能会小于 byte 切片的长度,但不会出现大于的情况。最后,不管什么情况,都不能修改 byte 切片里的数据。
type Reader interface {
Read(p []byte) (n int, err error)
}
这个方法接受一个 byte 切片,并返回两个值。第一个值是读入的字节数,第二个值是 error 错误值。
(1) Read 最多读入 len(p)字节,保存到 p。这个方法返回读入的字节数(0 <= n <= len(p))和任何读取时发生的错误。即便 Read 返回的 n < len(p),方法也可能使用所有 p 的空间存储临时数据。如果数据可以读取,但是字节长度不足 len(p),习惯上 Read 会立刻返回可用的数据,而不等待更多的数据。
(2) 当成功读取 n > 0 字节后,如果遇到错误或者文件读取完成,Read 方法会返回读入的字节数。方法可能会在本次调用返回一个非 nil 的错误,或者在下一次调用时返回错误(同时 n == 0)。这种情况的的一个例子是,在输入的流结束时,Read 会返回非零的读取字节数,可能会返回 err == EOF,也可能会返回 err == nil。无论如何,下一次调用 Read 应该返回 0, EOF。
(3) 调用者在返回的 n > 0 时,总应该先处理读入的数据,再处理错误 err。这样才能正确操作读取一部分字节后发生的 I/O 错误。EOF 也要这样处理。
(4) Read 的实现不鼓励返回 0 个读取字节的同时,返回 nil 值的错误。调用者需要将这种返回状态视为没有做任何操作,而不是遇到读取结束。
标准库里列出了实现 Read 方法的 4 条规则。第一条规则表明,该实现需要试图读取数据来填满被传入的 byte 切片。允许出现读取的字节数小于 byte 切片的长度,并且如果在读取时已经读到数据但是数据不足以填满 byte 切片时,不应该等待新数据,而是要直接返回已读数据。
第二条规则提供了应该如何处理达到文件末尾(EOF)的情况的指导。当读到最后一个字节时,可以有两种选择。一种是 Read 返回最终读到的字节数,并且返回 EOF 作为错误值,另一种是返回最终读到的字节数,并返回 nil 作为错误值。在后一种情况下,下一次读取的时候,由于没有更多的数据可供读取,需要返回 0 作为读到的字节数,以及 EOF 作为错误值。
第三条规则是给调用 Read 的人的建议。任何时候 Read 返回了读取的字节数,都应该优先处理这些读取到的字节,再去检查 EOF 错误值或者其他错误值。
最终,第四条约束建议 Read方法的实现永远不要返回 0 个读取字节的同时返回 nil 作为错误值。如果没有读到值,Read 应该总是返回一个错误。
整合并完成工作
// 使用 io.Writer 接口的
package main
import (
"bytes"
"fmt"
"os"
)
// main 是应用程序的入口
func main() {
// 创建一个 Buffer 值,并将一个字符串写入 Buffer
// 使用实现 io.Writer 的 Write 方法
var b bytes.Buffer
b.Write([]byte("Hello "))
// 使用 Fprintf 来将一个字符串拼接到 Buffer 里
// 将 bytes.Buffer 的地址作为 io.Writer 类型值传入
fmt.Fprintf(&b, "World!")
// 将 Buffer 的内容输出到标准输出设备
// 将 os.File 值的地址作为 io.Writer 类型值传入
b.WriteTo(os.Stdout)
}
简单的 curl
// 这个示例程序展示如何使用 io.Reader 和 io.Writer 接口
// 写一个简单版本的 curl
package main
import (
"io"
"log"
"net/http"
"os"
)
// main 是应用程序的入口
func main() {
// 这里的 r 是一个响应,r.Body 是 io.Reader
r, err := http.Get(os.Args[1])
if err != nil {
log.Fatalln(err)
}
// 创建文件来保存响应内容
file, err := os.Create(os.Args[2])
if err != nil {
log.Fatalln(err)
}
defer file.Close()
// 使用 MultiWriter,这样就可以同时向文件和标准输出设备
// 进行写操作
dest := io.MultiWriter(os.Stdout, file)
// 读出响应的内容,并写到两个目的地
io.Copy(dest, r.Body)
if err := r.Body.Close(); err != nil {
log.Println(err)
}
}
可以在 io 包里找到大量的支持不同功能的函数,这些函数都能通过实现了 io.Writer 和io.Reader 接口类型的值进行调用。其他包,如 http 包,也使用类似的模式,将接口声明为包的 API 的一部分,并提供对 io 包的支持。应该花时间看一下标准库中提供了些什么,以及它是如何实现的——不仅要防止重新造轮子,还要理解 Go 语言的设计者的习惯,并将这些习惯应用到自己的包和 API 的设计上。