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 服务器都提供 JSON 接口,通过发送HTTP请求来获取想要得到的JSON信息。下面通过查询Github提供的 issue 跟踪接口来演示一下。
首先,定义好类型,顺便还有常量:
// ch4/github/github.go
// https://api.github.com/ 提供了丰富的接口
// 提供查询GitHub的issue接口的API
// GitHub上有详细的API使用说明:https://developer.github.com/v3/search/#search-issues-and-pull-requests
package github
import "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的查询返回的信息是相当多的。
函数 SearchIssues 发送HTTP请求并将返回的JSON字符串进行解析。
关于Get请求的参数,参数中可能会出现URL格式里的特殊字符,比如 ?、&。因此要使用 url.QueryEscape 函数进行转义。
// ch4/github/search.go
package github
import (
"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 中的内容进行格式化输出,这里用了固定宽度的方法将结果输出为类似表格的形式:
// ch4/issues/main.go
// 将符合条件的issue输出为一个表格
package main
import (
"fmt"
"gopl/ch4/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\ch4\issues> go run main.go repo:golang/go is:open json decoder tag
6 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, l
PS H:\Go\src\gopl\ch4\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 main
import (
"log"
"os"
"text/template"
"time"
"gopl/ch4/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)
}
}
这个版本还可以改善,下面对解析错误的处理进行了改进
由于目标通常是在编译期间就固定下来的,因此无法解析将会是一个严重的bug。上面的版本如果无法解析(去掉个大括号试试),只会以比较温和的方式报告出来。
这里推荐使用帮助函数 template.Must,模板错误会Panic:
package main
import (
"log"
"os"
"text/template"
"time"
"gopl/ch4/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\ch4\issuesreport> go run main.go repo:golang/go is:open json decoder tag
6 issues:
----------------------------------------
Number: 28143
User: Carpetsmoker
Title: proposal: encoding/json: add "readonly" tag
Age: 135 days
----------------------------------------
Number: 14750
User: cyberphone
Title: encoding/json: parser ignores the case of member names
Age: 1079 days
----------------------------------------
...
接着看 html/template 包。它使用和 text/template 包里一样的 API 和表达式语法,并且额外地对出现在 HTML、JavaScript、CSS 和 URL 中的字符串进行自动转义。这样可以避免在生成 HTML 是引发一些安全问题。
下面是一个将 issue 输出为 HTML 表格代码。由于两个包里的API是一样的,所以除了模板本身以外,GO代码没有太大的差别:
package main
import (
"fmt"
"log"
"net/http"
"os"
)
import (
"gopl/ch4/github"
"html/template"
)
var issueList = template.Must(template.New("issuelist").Parse(`
{{.TotalCount}} issues
#
State
User
Title
{{range .Items}}
{{.Number}}
{{.State}}
{{.User.Login}}
{{.Title}}
{{end}}
`))
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)
}
}
通过模板的操作导入的字符串,默认都会按照原样显示出来。就是会把HTML的特殊字符自动进行转义,效果就是无法通过模板导入的内容生成html标签。
如果就是需要通过模板的操作再导入一些HTML的内容,就需要使用 template.HTML 类型。使用 template.HTML 类型后,可以避免模板自动转义受信任的 HTML 数据。同样的类型还有 template.CSS、template.JS、template.URL 等,具体可以查看源码。
下面的操作演示了普通的 string 类型和 template.HTML 类型在导入一个 HTML 标签后显示效果的差别:
package main
import (
"fmt"
"html/template"
"log"
"net/http"
)
func main() {
const templ = `A: {{.A}}
B: {{.B}}
`
t := template.Must(template.New("escape").Parse(templ))
var data struct {
A string // 不受信任的纯文本
B template.HTML // 受信任的HTML
}
data.A = "Hello!"
data.B = "Hello!"
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))
}
转载于:https://blog.51cto.com/steed/2353945