JSON

JSON是一种发送和接收格式化信息的标准。JSON不是唯一的标准,XML、ASN.1 和 Google 的 Protocol Buffer 都是相似的标准。Go通过标准库 encoding/json、encoding/xml、encoding/asn1 和其他的库对这些格式的编码和解码提供了非常好的支持,这些库都拥有相同的API。

序列化输出

首先定义一组数据:

type Movie struct { Title string Year int `json:"released"` Color bool `json:"color,omitempty"` Actors []string}var movies = []Movie{ {Title: "Casablanca", Year: 1942, Color: false, Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}}, {Title: "Cool Hand Luke", Year: 1967, Color: true, Actors: []string{"Paul Newman"}}, {Title: "Bullitt", Year: 1968, Color: true, Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},}

然后通过 json.Marshal 进行编码:

data, err := json.Marshal(movies)if err != nil { log.Fatalf("JSON Marshal failed: %s", err)}fmt.Printf("%s\n", data)/* 执行结果[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingrid Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Actors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"Actors":["Steve McQueen","Jacqueline Bisset"]}]*/

这种紧凑的表示方法适合传输,但是不方便阅读。有一个 json.MarshalIndent 的变体可以输出整齐格式化过的结果。多传2个参数,第一个是定义每行输出的前缀字符串,第二个是定义缩进的字符串:

data, err := json.MarshalIndent(movies, "", " ")if err != nil { log.Fatalf("JSON Marshal failed: %s", err)}fmt.Printf("%s\n", data)/* 执行结果[ { "Title": "Casablanca", "released": 1942, "Actors": [ "Humphrey Bogart", "Ingrid Bergman" ] }, { "Title": "Cool Hand Luke", "released": 1967, "color": true, "Actors": [ "Paul Newman" ] }, { "Title": "Bullitt", "released": 1968, "color": true, "Actors": [ "Steve McQueen", "Jacqueline Bisset" ] }]*/

只有可导出的成员可以转换为JSON字段,上面的例子中用的都是大写。
成员标签(field tag),是结构体成员的编译期间关联的一些元素信息。标签值的第一部分指定了Go结构体成员对应的JSON中字段的名字。
另外,Color标签还有一个额外的选项 omitempty,它表示如果这个成员的值是零值或者为空,则不输出这个成员到JSON中。所以Title为"Casablanca"的JSON里没有color。

反序列化

反序列化操作将JSON字符串解码为Go数据结构。这个是由 json.Unmarshal 实现的。

var titles []struct{ Title string }if err := json.Unmarshal(data, &titles); err != nil { log.Fatalf("JSON unmarshaling failed: %s", err)}fmt.Println(titles)/* 执行结果[{Casablanca} {Cool Hand Luke} {Bullitt}]*/

这里接收数据时定义的结构体只有一个Title字段,这样当函数 Unmarshal 调用完成后,将填充结构体切片中的 Title 值,而JSON中其他的字段就丢弃了。

Web 应用

很多的 Web 服务器都提供 JSON 接口,通过发送HTTP请求来获取想要得到的JSON信息。下面通过查询Github提供的 issue 跟踪接口来演示一下。

定义结构体

首先,定义好类型,顺便还有常量:

// ch5/github/github.go// https://api.github.com/ 提供了丰富的接口// 提供查询GitHub的issue接口的API// GitHub上有详细的API使用说明:https://developer.github.com/v3/search/#search-issues-and-pull-requestspackage githubimport "time"const IssuesURL = "https://api.github.com/search/issues"type IssuesSearchResult struct { TotalCount int `json:"total_count"` Items []*Issue}type Issue struct { Number int HTMLURL string `json:"html_url"` Title string State string User *User CreateAt time.Time `json:"created_at"` Body string // Markdown 格式}type User struct { Login string HTMLURL string `json:"html_url"`}

关于字段名称,即使对应的JSON字段的名称都是小写的,但是结构体中的字段必须首字母大写(不可导出的字段也无法把JSON数据导入)。这种情况很普遍,这里可以偷个懒。在 Unmarshal 阶段,JSON字段的名称关联到Go结构体成员的名称是忽略大小写的,这里也不需要考虑序列化的问题,所以很多地方都不需要写成员标签。不过,小写的变量在需要分词的时候,可能会使用下划线分割,这种情况下,还是要用一下成员标签的。
这里也是选择性地对JSON中的字段进行解码,因为相对于这里演示的内容,GitHub的查询返回的信息是相当多的。

请求获取JSON并解析

函数 SearchIssues 发送HTTP请求并将返回的JSON字符串进行解析。
关于Get请求的参数,参数中可能会出现URL格式里的特殊字符,比如 ?、&。因此要使用 url.QueryEscape 函数进行转义。

// ch5/github/search.gopackage githubimport ( "encoding/json" "fmt" "net/http" "net/url" "strings")// 查询GitHub的issue接口func SearchIssues(terms []string) (*IssuesSearchResult, error) { q := url.QueryEscape(strings.Join(terms, " ")) resp, err := http.Get(IssuesURL + "?q=" + q) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("search query failed: %s", resp.Status) } var result IssuesSearchResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return &result, nil}

流式解码噐
之前是使用 json.Unmarshal 进行解码,而这里使用流式解码噐。它可以依次从字节流中解码出多个JSON实体,不过这里没有用到该功能。另外还有对应的 json.Encoder 的流式编码器。
调用 Decode 方法后,就完成了对变量 result 的填充。

调用执行

最后就是将 result 中的内容进行格式化输出,这里用了固定宽度的方法将结果输出为类似表格的形式:

// ch5/issues/main.go// 将符合条件的issue输出为一个表格package mainimport ( "fmt" "gopl/ch5/github" "log" "os")func main() { result, err := github.SearchIssues(os.Args[1:]) if err != nil { log.Fatal(err) } fmt.Printf("%d issue: \n", result.TotalCount) for _, item := range result.Items { fmt.Printf("#%-5d %9.9s %.55s\n", item.Number, item.User.Login, item.Title) }}

使用命令行参数指定搜索条件,该命令搜索 Go 项目里的 issue 接口,查找 open 状态的列表。由于返回的还是很多,后面的参数是对内容再进行筛选:

PS H:\Go\src\gopl\ch5\issues> go run main.go repo:golang/go is:open json decoder tag6 issue:#28143 Carpetsmo proposal: encoding/json: add "readonly" tag#14750 cyberphon encoding/json: parser ignores the case of member names#17609 nathanjsw encoding/json: ambiguous fields are marshalled#22816 ganelon13 encoding/json: include field name in unmarshal error me#19348 davidlaza cmd/compile: enable mid-stack inlining#19109 bradfitz proposal: cmd/go: make fuzzing a first class citizen, lPS H:\Go\src\gopl\ch5\issues>文本模板

进行简单的格式化输出,使用fmt包就足够了。但是要实现更复杂的格式化输出,并且有时候还要求格式和代码彻底分离。这可以通过 text/templat 包和 html/template 包里的方法来实现,通过这两个包,可以将程序变量的值代入到模板中。

模板表达式

模板,是一个字符串或者文件,它包含一个或者多个两边用双大括号包围的单元,这称为操作。大多数字符串是直接输出的,但是操作可以引发其他的行为。
每个操作在模板语言里对应一个表达式,功能包括:

输出值选择结构体成员调用函数和方法描述控逻辑实例化其他的模板

这篇里有表达式的介绍: https://blog.51cto.com/steed/2321827

继续使用 GitHub 的 issue 接口返回的数据,这次使用模板来输出。一个简单的字符串模板如下所示:

const templ = `{{.TotalCount}} issues:{{range .Items}}----------------------------------------Number: {{.Number}}User: {{.User.Login}}Title: {{.Title | printf "%.64s"}}Age: {{.CreatedAt | daysAgo}} days{{end}}`

点号(.)表示当前值的标记。最开始的时候表示模板里的参数,也就是 github.IssuesSearchResult。
操作 {{.TotalCount}} 就是 TotalCount 字段的值。
{{range .Items}} 和 {{end}} 操作创建一个循环,这个循环内部的点号(.)表示Items里的每一个元素。
在操作中,管道符(|)会将前一个操作的结果当做下一个操作的输入,这个和UNIX里的管道类似。
{{.Title | printf "%.64s"}},这里的第二个操作是printf函数,在包里这个名称对应的就是fmt.Sprintf,所以会按照fmt.Sprintf函数返回的样式输出。
{{.CreatedAt | daysAgo}},这里的第二个操作数是 daysAgo,这是一个自定义的函数,具体如下:

func daysAgo(t time.Time) int { return int(time.Since(t).Hours() / 24)}模板输出的过程

通过模板输出结果需要两个步骤:

解析模板并转换为内部表示的方法在指定的输入上执行(就是执行并输出)

解析模板只需要执行一次。下面的代码创建并解析上面定义的文本模板:

report, err := template.New("report"). Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ)if err != nil { log.Fatal(err)}

这里使用了方法的链式调用。template.New 函数创建并返回一个新的模板。
Funcs 方法将自定义的 daysAgo 函数到内部的函数列表中。之前提到的printf实际对应的是fmt.Sprintf,也是在包内默认就已经在这个函数列表里了。如果有更多的自定义函数,就多次调用这个方法添加。
最后就是调用Parse进行解析。
上面的代码完成了创建模板,添加内部可调用的 daysAgo 函数,解析(Parse方法),检查(检查err是否为空)。现在就可以调用report的 Execute 方法,传入数据源(github.IssuesSearchResult,这个需要先调用github.SearchIssues函数来获取),并指定输出目标(使用 os.Stdout):

if err := report.Execute(os.Stdout, result); err != nil { log.Fatal(err)}

之前的代码比较凌乱,下面出完整可运行的代码:

package mainimport ( "log" "os" "text/template" "time" "gopl/ch5/github")const templ = `{{.TotalCount}} issues:{{range .Items}}----------------------------------------Number: {{.Number}}User: {{.User.Login}}Title: {{.Title | printf "%.64s"}}Age: {{.CreatedAt | daysAgo}} days{{end}}`// 自定义输出格式的方法func daysAgo(t time.Time) int { return int(time.Since(t).Hours() / 24)}func main() { // 解析模板 report, err := template.New("report"). Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ) if err != nil { log.Fatal(err) } // 获取数据 result, err := github.SearchIssues(os.Args[1:]) if err != nil { log.Fatal(err) } // 输出 if err := report.Execute(os.Stdout, result); err != nil { log.Fatal(err) }}

这个版本还可以改善,下面对解析错误的处理进行了改进

帮助函数 Must

由于目标通常是在编译期间就固定下来的,因此无法解析将会是一个严重的bug。上面的版本如果无法解析(去掉个大括号试试),只会以比较温和的方式报告出来。
这里推荐使用帮助函数 template.Must,模板错误会Panic:

package mainimport ( "log" "os" "text/template" "time" "gopl/ch5/github")const templ = `{{.TotalCount}} issues:{{range .Items}}----------------------------------------Number: {{.Number}}User: {{.User.Login}}Title: {{.Title | printf "%.64s"}}Age: {{.CreatedAt | daysAgo}} days{{end}}`// 自定义输出格式的方法func daysAgo(t time.Time) int { return int(time.Since(t).Hours() / 24)}// 使用帮助函数var report = template.Must(template.New("issuelist"). Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ))func main() { result, err := github.SearchIssues(os.Args[1:]) if err != nil { log.Fatal(err) } if err := report.Execute(os.Stdout, result); err != nil { log.Fatal(err) }}

和上个版本的区别就是解析的过程外再包了一层 template.Must 函数。而效果就是原本解析错误是调用 log.Fatal(err) 来退出,这个调用也是自己的代码里指定的。
而现在是调用 panic(err) 来退出,并且会看到一个更加严重的错误报告(错误信息是一样的),并且这个也是包内部提供的并且推荐的做法。
最后是输出的结果:

PS H:\Go\src\gopl\ch5\issuesreport> go run main.go repo:golang/go is:open json decoder tag6 issues:----------------------------------------Number: 28143User: CarpetsmokerTitle: proposal: encoding/json: add "readonly" tagAge: 135 days----------------------------------------Number: 14750User: cyberphoneTitle: encoding/json: parser ignores the case of member namesAge: 1079 days----------------------------------------...HTML 模板

接着看 html/template 包。它使用和 text/template 包里一样的 API 和表达式语法,并且额外地对出现在 HTML、JavaScript、CSS 和 URL 中的字符串进行自动转义。这样可以避免在生成 HTML 是引发一些安全问题。

使用模板输出页面

下面是一个将 issue 输出为 HTML 表格代码。由于两个包里的API是一样的,所以除了模板本身以外,GO代码没有太大的差别:

package mainimport ( "fmt" "log" "net/http" "os")import ( "gopl/ch5/github" "html/template")var issueList = template.Must(template.New("issuelist").Parse(`<h2>{{.TotalCount}} issues</h2><table><tr style='text-align: left'> <th>#</th> <th>State</th> <th>User</th> <th>Title</th></tr>{{range .Items}}<tr> <td><a href='{{.HTMLURL}}'>{{.Number}}</a></td> <td>{{.State}}</td> <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td> <td>{{.Title}}</td></tr>{{end}}</table>`))func main() { result, err := github.SearchIssues(os.Args[1:]) if err != nil { log.Fatal(err) } fmt.Println("http://localhost:8000") handler := func(w http.ResponseWriter, r *http.Request) { showIssue(w, result) } http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe("localhost:8000", nil))}func showIssue(w http.ResponseWriter, result *github.IssuesSearchResult) { if err := issueList.Execute(w, result); err != nil { log.Fatal(err) }}template.HTML 类型

通过模板的操作导入的字符串,默认都会按照原样显示出来。就是会把HTML的特殊字符自动进行转义,效果就是无法通过模板导入的内容生成html标签。
如果就是需要通过模板的操作再导入一些HTML的内容,就需要使用 template.HTML 类型。使用 template.HTML 类型后,可以避免模板自动转义受信任的 HTML 数据。同样的类型还有 template.CSS、template.JS、template.URL 等,具体可以查看源码。
下面的操作演示了普通的 string 类型和 template.HTML 类型在导入一个 HTML 标签后显示效果的差别:

package mainimport ( "fmt" "html/template" "log" "net/http")func main() { const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>` t := template.Must(template.New("escape").Parse(templ)) var data struct { A string // 不受信任的纯文本 B template.HTML // 受信任的HTML } data.A = "<b>Hello!</b>" data.B = "<b>Hello!</b>" fmt.Println("http://localhost:8000") handler := func(w http.ResponseWriter, r *http.Request) { if err := t.Execute(w, data); err != nil { log.Fatal(err) } } http.HandleFunc("/", handler) log.Fatal(http.ListenAndServe("localhost:8000", nil))}