Go入门(4):标准库

  • A+
所属分类:学习笔记

第八章学习笔记

Go标准库是一组核心包,用来扩展和增强语言的能力。这些包为语言增加了大量不同的类型。开发人员可以直接使用这些类型,而不用再写自己的包或者去下载其他人发布的第三方包。由于这些包和语言绑在一起发布,它们会得到以下特殊的保证:

每次语言更新,哪怕是小更新,都会带有标准库;

这些标准库会严格遵守向后兼容的承诺;

标准库是Go语言开发、构建、发布过程的一部分;

标准库由Go的构建者们维护和评审;

每次Go语言发布新版本时,标准库都会被测试,并评估性能。

使用标准库里的包可以使管理代码变得更容易,并且保证代码的稳定。不用担心程序无法兼容不同的Go语言版本,也不用管理第三方依赖。

一、源代码和文档

1.标准库里的顶级目录和包
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

2.如果想了解所有包以及更详细的描述,Go语言团队在网站上维护了一个文档,参见http://golang.org/pkg/。golang网站的pkg页面提供了每个包的godoc文档。

如果想以交互的方式浏览文档,Sourcegraph索引了所有标准库的代码,以及大部分包含Go代码的公开库。

3.不管用什么方式安装Go,标准库的源代码都会安装在$GOROOT/src/pkg文件夹中。

作为Go发布包的一部分,标准库的源代码是经过预编译的。这些预编译后的文件,称作归档文件(archive file),扩展名是.a, 可以 在$GOROOT/pkg文件夹中找到已经安装的各目标平台和操作系统的归档文件。这些文件是特殊的Go静态库文件,由Go的构建工具创建,并在编译和链接最终程序时被使用。归档文件可以让构建的速度更快。但是在构建的过程中,没办法指定这些文件,所以没办法与别人共享这些文件。Go工具链知道什么时候可以使用已有的.a 文件,什么时候需要从机器上的源代码重新构建。

二、记录日志

1.在UNIX里,日志有很长的历史。这些积累下来的经验都体现在log包的设计里。传统的CLI(命令行界面)程序直接将输出写到名为stdout的设备上。所有的操作系统上都有这种设备,这种设备的默认目的地是标准文本输出。默认设置下,终端会显示这些写到stdout设备上的文本。这种单个目的地的输出用起来很方便,不过你总会碰到需要同时输出程序信息和输出执行细节的情况。这些执行细节被称作日志。当想要记录日志时,你希望能写到不同的目的地,这样就不会将程序的输出和日志混在一起了。
为了解决这个问题,UNIX架构上增加了一个叫作stderr的设备。这个设备被创建为日志的默认目的地。这样开发人员就能将程序的输出和日志分离开来。如果想在程序运行时同时看到程序输出和日志,可以将终端配置为同时显示写到stdout和stderr的信息。不过,如果用户的程序只记录日志,没有程序输出,更常用的方式是将一般的日志信息写到stdout,将错误或者警告信息写到stderr。

2.log包使用
A.配置以及原理介绍
通常程序会在这个init()函数里配置日志参数,这样程序一开始就能使用log包进行正确的输出。

log.SetPrefix("TRACE: ")设置了一个字符串,作为每个日志项的前缀。这个字符串应该是能让用户从一般的程序输出中分辨出日志的字符串。传统上这个字符串的字符会全部大写。

有几个和log包相关联的常量,这些常量用来控制可以写到每个日志项的其他信息。
Ldate、Ltime、Lmicroseconds、Llongfile、Lshortfile、LstdFlags等

其中Ldate= 1 << iota
关键字iota在常量声明区里有特殊的作用。这个关键字让编译器为每个常量复制相同的表
达式,直到声明区结束,或者遇到一个新的赋值语句。关键字iota的另一个功能是,iota的
初始值为0,之后iota的值在每次处理为常量后,都会自增1。

使用关键字iota
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。

func init(){
...
log.SetFlags(log.Ldate| log.Lmicroseconds| log.Llongfile)
}
这里我们将Ldate、Lmicroseconds和Llongfile标志组合在一起,将该操作的值传入SetFlags函数。这些标志值组合在一起后,最终的值是13,代表第1、3和4位为1(00001101)。由于每个常量表示单独一个位,这些标志经过或操作组合后的值,可以表示每个需要的日志参数。之后log包会按位检查这个传入的整数值,按照需求设置日志项记录的信息。

B.使用说明
// Println写到标准日志记录器
log.Println("message")

// Fatalln在调用Println()之后会接着调用os.Exit(1)
log.Fatalln("fatal message")

// Panicln在调用Println()之后会接着调用panic()
log.Panicln("panic message")

C.多goroutine安全
log包有一个很方便的地方就是,这些日志记录器是多goroutine安全的。这意味着在多个
goroutine可以同时调用来自同一个日志记录器的这些函数,而不会有彼此间的写冲突。标准日志
记录器具有这一性质,用户定制的日志记录器也应该满足这一性质

3.定制日志记录器
要想创建一个定制的日志记录器,需要创建一个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")
}
说明:a.这段程序创建了4种不同的Logger类型的指针变量,分别命名为Trace、Info、Warning和Error。每个变量使用不同的配置,用来表示不同的重要程度。
b.ioutil包里的Discard变量作为写到的目的地
c.函数调用会返回一个io.Writer接口类型值,这个值包含之前打开的文件file,以及stderr。MultiWriter函数是一个变参函数,可以接受任意个实现了io.Writer接口的值。这个函数会返回一个io.Writer值,这个值会把所有传入的io.Writer的值绑在一起。当对这个返回值进行写入时,会向所有绑在一起的io.Writer值做写入。这让类似log.New这样的函数可以同时向多个Writer做输出。现在,当我们使用Error记录器记录日志时,输出会同时写到文件和stderr。

三、编码/解码

如果程序需要处理XML或者JSON,可以使用标准库里名为xml和json的包,它们可以处理这些格式的数据。如果想实现自己的数据格式的编解码,可以将这些包的实现作为指导。

JSON远比XML流行。
这主要是因为与XML相比,使用JSON需要处理的标签更少。而这就意味着网络传输时每个消息的数据更少,从而提升整个系统的性能。
JSON可以转换为BSON(Binary JavaScript Object Notation,二进制JavaScript对象标记),进一步缩小每个消息的数据长度。

1. 解码 JSON
使用json包的NewDecoder函数以及Decode方法进行解码。

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"`
}
)
gResponse和gResult的类型声明,你会注意到每个字段最后使用单引号声明了一个字符串。这些字符串被称作标签(tag),是提供每个字段的元信息的一种机制,将JSON文档和结构类型里的字段一一映射起来。如果不存在标签,编码和解码过程会试图以大小写无关的方式,直接使用字段的名字进行匹配。如果无法匹配,对应的结构类型里的字段就包含其零值。

2.编码 JSON
处理JSON的第二个方面是,使用json包的MarshalIndent函数进行编码。这个函数可以很方便地将Go语言的map类型的值或者结构类型的值转换为易读格式的JSON文档。序列化(marshal)是指将数据转换为JSON字符串的过程。

四、输入和输出

类UNIX的操作系统如此伟大的一个原因是,一个程序的输出可以是另一个程序的输入这一理念。依照这个哲学,这类操作系统创建了一系列的简单程序,每个程序只做一件事,并把这件事做得非常好。之后,将这些程序组合在一起,可以创建一些脚本做一些很惊艳的事情。这些程序使用 stdin 和 stdout 设备作为通道,在进程之间传递数据。——智能合约也很伟大
同样的理念扩展到了标准库的 io 包,而且提供的功能很神奇。这个包可以以流的方式高效处理数据,而不用考虑数据是什么,数据来自哪里,以及数据要发送到哪里的问题。与 stdout和 stdin 对应,这个包含有 io.Writer 和 io.Reader 两个接口。所有实现了这两个接口的类型的值,都可以使用 io 包提供的所有功能,也可以用于其他包里接受这两个接口的函数以及方法。这是用接口类型来构造函数和 API 最美妙的地方。开发人员可以基于这些现有功能进行组合,利用所有已经存在的实现,专注于解决业务问题。

1.Writer 和 Reader 接口
io 包是围绕着实现了 io.Writer 和 io.Reader 接口类型的值而构建的。

io按照一定的规则实现了Writer 、Read方法。

示例参照listing37.go

2.简单的curl
在Linux和MacOS(曾用名Mac OS X)系统里可以找到一个名为curl的命令行工具。这个工具可以对指定的URL发起HTTP请求,并保存返回的内容。通过使用http、io和os包,我们可以用很少的几行代码来实现一个自己的curl工具。
示例代码:listing46.go

可以在 io 包里找到大量的支持不同功能的函数,这些函数都能通过实现了 io.Writer 和io.Reader 接口类型的值进行调用。其他包,如 http 包,也使用类似的模式,将接口声明为包的 API 的一部分,并提供对 io 包的支持。
应该花时间看一下标准库中提供了些什么,以及它是如何实现的——不仅要防止重新造轮子,还要理解 Go 语言的设计者的习惯,并将这些习惯应用到自己的包和 API 的设计上。

五、总结

标准库有特殊的保证,并且被社区广泛应用。

使用标准库的包会让你的代码更易于管理,别人也会更信任你的代码。

100 余个包被合理组织,分布在 38 个类别里。

标准库里的 log 包拥有记录日志所需的一切功能。

标准库里的 xml 和 json 包让处理这两种数据格式变得很简单。

io 包支持以流的方式高效处理数据。

接口允许你的代码组合已有的功能。

阅读标准库的代码是熟悉 Go 语言习惯的好方法

发表评论

您必须才能发表评论!