本章主要内容
- 输出数据以及记录日志
- 对 JSON 进行编码和解码
- 处理输入/输出,并以流的方式处理数据
- 让标准库里多个包协同工作
什么是Go语言标准库?
- Go 标准库是一组核心包,用来扩展和增强语言的能力。这些包为语言增加了大量不同的类型。开发人员可以直接使用这些类型,而不用再写自己的包或者去下载其他人发布的第三方包。
- 标准库本身是经过良好设计的,并且比其他语言的标准库提供了更多的功能。
1. 文档与源码
标准库里包含众多的包,总共超过100个包,这些包被分到38个类别里。如下所示:
$GOROOT/src/pkg归档文件$GOROOT/pkg
2. 日志
logstdout
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()
log.Fatalln("fatal message")
//Panicln在调用Println()之后会接着调用panic()
log.Panicln("panic message")
}
- 通常程序会在这个 init()函数里配置日志参数,这样程序一开始就能使用 log 包进行正确的输出。
- 有几个和 log 包相关联的标志,这些标志用来控制可以写到每个日志项的其他信息。
log包的源码如下(src/log/log.go):
// These flags define which text to prefix to each log entry generated by the Logger.
// Bits are or'ed together to control what's printed.
// With the exception of the Lmsgprefix flag, there is no
// control over the order they appear (the order listed here)
// or the format they present (as described in the comments).
// The prefix is followed by a colon only when Llongfile or Lshortfile
// is specified.
// For example, flags Ldate | Ltime (or LstdFlags) produce,
// 2009/01/23 01:23:23 message
// while flags Ldate | Ltime | Lmicroseconds | Llongfile produce,
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
Llongfile // full file name and line number: /a/b/c/d.go:23
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
Lmsgprefix // move the "prefix" from the beginning of the line to before the message
LstdFlags = Ldate | Ltime // initial values for the standard logger
)
log 包
2.1 定制的日志记录器
要想创建一个定制的日志记录器,需要创建一个 Logger 类型值。可以给每个日志记录器配置一个单独的目的地,并独立设置其前缀和标志。
var (
Trace *log.Logger // 记录所有日志
Info *log.Logger // 重要的信息
Warning *log.Logger // 需要注意的信息
Error *log.Logger //非常严重的问题
)
为了创建每个日志记录器,我们使用了 log 包的 New 函数,它创建并正确初始化一个Logger 类型的值。
// New creates a new Logger. The out variable sets the
// destination to which log data will be written.
// The prefix appears at the beginning of each generated log line, or
// after the log header if the Lmsgprefix flag is provided.
// The flag argument defines the logging properties.
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
ioutil 包Discard 变量
// src/io/ioutil/ioutil.go
// devNull是一个用int作为基础类型的类型
type devNull int
// Discard is an io.Writer on which all Write calls succeed
// without doing anything.
// Discard是一个io.Writer,所有的Writer调用都不会有动作,但是会成功返回
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 变量可以禁用这个等级的日志。
func init() {
file, err := os.OpenFile("error.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")
}
- 日志记录器 Info 和 Warning 都使用 stdout 作为日志输出,变量 Stdout 的声明也有一些有意思的地方,源码如下:
// Stdin, Stdout, and Stderr are open Files pointing to the standard input,
// standard output, and standard error file descriptors.
//
// Note that the Go runtime writes to standard error for panics and crashes;
// closing Stderr may cause those messages to go elsewhere, perhaps
// to a file opened later.
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
// NewFile源码:src/os/file_windows.go
// NewFile returns a new File with the given file descriptor and
// name. The returned value will be nil if fd is not a valid file
// descriptor.
func NewFile(fd uintptr, name string) *File {
h := syscall.Handle(fd)
if h == syscall.InvalidHandle {
return nil
}
return newFile(h, name, "file")
}
- Stdin、Stdout 和 Stderr,这 3 个变量都被声明为 File 类型的指针,这个类型实现了 io.Writer 接口。
- MultiWriter 函数是一个变参函数,可以接受任意个实现了 io.Writer 接口的值。这个函数会返回一个 io.Writer 值,这个值会把所有传入的 io.Writer 的值绑在一起。当对这个返回值进行写入时,会向所有绑在一起的io.Writer 值做写入。让类似 log.New 这样的函数可以同时向多个 Writer 做输出。这里,当使用 Error 记录器记录日志时,输出会同时写到文件和 stderr。
为 Logger 类型实现的所有方法:
2. 编码、解码
xmljson
2.1 解码JSON
- 要学习的处理 JSON 的第一个方面是,使用 json 包的 NewDecoder 函数以及 Decode方法进行解码。示例代码如下:
package main
import (
"encoding/json"
"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
}
log.Println(gr)
}
标签(tag)
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": "111.111.111.111",
"cell":"122.122.122.122"
}
}`
var c Contact
err2 := json.Unmarshal([]byte(JSON), &c)
if err2 != nil {
log.Println("ERROR", err2)
return
}
var c map[string]interface{}
// 将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("H:", c["contact"].(map[string]interface{})["home"]) //展示了如何将 contact 键的值转换为另一个键是 string 类型,值是interface{}类型的 map 类型。
2.2 编码JSON
MarshalIndent
package main
import (
"encoding/json"
"log"
)
//这个示例程序展示如何序列化JSON字符串
func main() {
// 创建一个保存键值对的映射
c := make(map[string]interface{})
c["name"] = "Gopher"
c["title"] = "programmer"
c["contact"] = map[string]interface{}{
"home": "1.2.3.4",
"cell": "6.8.9.0",
}
//将这个映射序列化到JSON字符串
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
log.Println("ERROR", err)
return
}
println(string(data))
}
- 函数 MarshalIndent 返回一个 byte 切片,用来保存 JSON 字符串和一个 error 值。json 包中 MarshalIndent 函数的声明如下:
// MarshalIndent is like Marshal but applies Indent to format the output.
// Each JSON element in the output will begin on a new line beginning with prefix
// followed by one or more copies of indent according to the indentation nesting.
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {
b, err := Marshal(v)
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = Indent(&buf, b, prefix, indent)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
反射Marshal
3. 输入和输出
io.Writerio.Reader
3.1 Writer 和 Reader 接口
io 包
//io.Writer接口的声明
// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream. It returns the number of bytes written from p (0 <= n <= len(p)) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len(p). Write must not modify the slice data, even temporarily.
// Implementations must not retain p.
type Writer interface {
Write(p []byte) (n int, err error)
}
- Write 从 p 里向底层的数据流写入 len§字节的数据。这个方法返回从 p 里写出的字节数(0 <= n <= len§),以及任何可能导致写入提前结束的错误。Write 在返回 n< len§的时候,必须返回某个非 nil 值的 error。Write 绝不能改写切片里的数据,哪怕是临时修改也不行。
- 这些规则意味着 Write 方法的实现需要试图写入被传入的 byte 切片里的所有数据。但是,如果无法全部写入,那么该方法就一定会返回一个错误。返回的写入字节数可能会小于 byte 切片的长度,但不会出现大于的情况。最后,不管什么情况,都不能修改 byte 切片里的数据。
// io.Reader接口的声明
// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch space during the call. If some data is available but not len(p) bytes, Read conventionally returns what is available instead of waiting for more.
// Read 最多读入 len(p)字节,保存到 p。这个方法返回读入的字节数(0 <= n <= len(p))和任何读取时发生的错误。即便 Read 返回的 n < len(p),方法也可能使用所有 p 的空间存储临时数据。如果数据可以读取,但是字节长度不足 len(p),习惯上 Read 会立刻返回可用的数据,而不等待更多的数据。
//
// When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, it returns the number of bytes read. It may return the (non-nil) error from the same call or return the error (and n == 0) from a subsequent call. An instance of this general case is that a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil. The next Read should return 0, EOF.
// 当成功读取 n > 0 字节后,如果遇到错误或者文件读取完成,Read 方法会返回读入的字节数。方法可能会在本次调用返回一个非 nil 的错误,或者在下一次调用时返回错误(同时 n =\= 0)。这种情况的的一个例子是,在输入的流结束时,Read 会返回非零的读取字节数,可能会返回 err =\= EOF,也可能会返回 err == nil。无论如何,下一次调用 Read 应该返回 0, EOF。
//
// Callers should always process the n > 0 bytes returned before considering the error err. Doing so correctly handles I/O errors that happen after reading some bytes and also both of the allowed EOF behaviors.
//调用者在返回的 n > 0 时,总应该先处理读入的数据,再处理错误 err。这样才能正确操作读取一部分字节后发生的 I/O 错误。EOF 也要这样处理。
//
// Implementations of Read are discouraged from returning a zero byte count with a nil error, except when len(p) == 0. Callers should treat a return of 0 and nil as indicating that nothing happened; in particular it does not indicate EOF.
//Read 的实现不鼓励返回 0 个读取字节的同时,返回 nil 值的错误。调用者需要将这种返回状态视为没有做任何操作,而不是遇到读取结束。
//
// Implementations must not retain p.
type Reader interface {
Read(p []byte) (n int, err error)
}
- 标准库里列出了实现 Read 方法的 4 条规则:
- 第一条规则表明,该实现需要试图读取数据来填满被传入的 byte 切片。允许出现读取的字节数小于 byte 切片的长度,并且如果在读取时已经读到数据但是数据不足以填满 byte 切片时,不应该等待新数据,而是要直接返回已读数据。
- 第二条规则提供了应该如何处理达到文件末尾(EOF)的情况的指导。当读到最后一个字节时,可以有两种选择。一种是 Read 返回最终读到的字节数,并且返回 EOF 作为错误值,另一种是返回最终读到的字节数,并返回 nil 作为错误值。在后一种情况下,下一次读取的时候,由于没有更多的数据可供读取,需要返回 0 作为读到的字节数,以及 EOF 作为错误值。
- 第三条规则是给调用 Read 的人的建议。任何时候 Read 返回了读取的字节数,都应该优先处理这些读取到的字节,再去检查 EOF 错误值或者其他错误值。
- 第四条约束建议 Read方法的实现永远不要返回 0 个读取字节的同时返回 nil 作为错误值。如果没有读到值,Read 应该总是返回一个错误。
3.2 整合并完成工作
bytesfmtosstdout
package main
import (
"bytes"
"fmt"
"os"
)
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)
}
Fprintf 函数
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
- 因为我们传入了之前创建的 Buffer 类型值的地址,这意味着 bytes 包里的 Buffer类型必须实现了这个接口。那么在 bytes 包的源代码里,我们应该能找到为 Buffer 类型声明的 Write 方法。
// Write appends the contents of p to the buffer, growing the buffer as needed. The return value n is the length of p; err is always nil. If the buffer becomes too large, Write will panic with ErrTooLarge.
// Write 将 p 的内容追加到缓冲区,如果需要,会增大缓冲区的空间。返回值 n 是p 的长度,err 总是 nil。如果缓冲区变得太大,Write 会引起崩溃
func (b *Buffer) Write(p []byte) (n int, err error) {
b.lastRead = opInvalid
m, ok := b.tryGrowByReslice(len(p))
if !ok {
m = b.grow(len(p))
}
return copy(b.buf[m:], p), nil
}
- 使用 WriteTo 方法将 Buffer 类型的变量的内容写到 stdout设备。这个方法接受一个实现了 io.Writer 接口的值。在这个程序里,传入的值是 os 包的Stdout 变量的值。
// 这些变量自动声明为 NewFile 函数返回的类型
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
// NewFile returns a new File with the given file descriptor and name. The returned value will be nil if fd is not a valid file descriptor.
// NewFile 返回一个具有给定的文件描述符和名字的新 File
func NewFile(fd uintptr, name string) *File {
h := syscall.Handle(fd)
if h == syscall.InvalidHandle {
return nil
}
return newFile(h, name, "file")
}
- os 包的源代码里,为File类型声明的 Write 方法
// Write writes len(b) bytes to the File. It returns the number of bytes written and an error, if any. Write returns a non-nil error when n != len(b).
// Write 将 len(b)个字节写入 File, 这个方法返回写入的字节数,如果有错误,也会返回错误, 如果 n != len(b),Write 会返回一个非 nil 的错误
func (f *File) Write(b []byte) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
n, e := f.write(b)
if n < 0 {
n = 0
}
if n != len(b) {
err = io.ErrShortWrite
}
epipecheck(f, e)
if e != nil {
err = f.wrapErr("write", e)
}
return n, err
}
3.3 实现一个简单的curl命令行工具
通过使用 http、io 和 os 包,可以用很少的几行代码来实现一个自己的 curl 工具。代码如下:
package main
import (
"io"
"log"
"net/http"
"os"
)
func main() {
//这里的r是一个响应,r.Body是io.Reader
// 使用来自命令行的第一个参数来执行HTTP Get请求, 如果这个参数是一个URL,而且请求没有发生错误,变量r里就包含了该请求的响应结果。
r, err := http.Get(os.Args[1])
if err != nil {
log.Fatalln(err)
}
// 创建文件来保存响应内容
// 使用命令行的第二个参数打开了一个文件. 如果这个文件打开成功, 在第23行会使用defer语句安排在函数退出时执行文件的关闭操作。
file, err := os.Create(os.Args[2])
if err != nil {
log.Fatalln(err)
}
defer file.Close()
//使用MultiWriter,这样就可以同时向文件和标准输出设备进行写操作
//使用 io包里的 MultiWriter 函数将文件和stdout整合为一个io.Writer值
dest := io.MultiWriter(os.Stdout, file)
//读出响应的内容,并写到两个目的地
// 使用io包的 Copy 函数从响应的结果里读取内容,并写入两个目的地
io.Copy(dest, r.Body)
if err := r.Body.Close(); err != nil {
log.Println(err)
}
}
结论:应该花时间看一下标准库中提供了些什么,以及它是如何实现的——不仅要防止重新造轮子,还要理解 Go 语言的设计者的习惯,并将这些习惯应用到自己的包和 API 的设计上。阅读标准库的代码是熟悉 Go 语言习惯的好方法。