Go语言标准库:记录日志、编/解码、输入输出

本章主要内容

  • 输出数据以及记录日志
  • 对 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 语言习惯的好方法。