JSON、文本模板、HTML模板

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

type Movie struct { Titlestring Yearint`json:"released"` Colorbool `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 跟踪接口来演示一下。
定义结构体 首先,定义好类型,顺便还有常量:
// 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 githubimport "time"const IssuesURL = "https://api.github.com/search/issues"type IssuesSearchResult struct { TotalCount int `json:"total_count"` Items[]*Issue }type Issue struct { Numberint HTMLURLstring `json:"html_url"` Titlestring Statestring User*User CreateAt time.Time `json:"created_at"` Bodystring// Markdown 格式 }type User struct { Loginstring HTMLURL string `json:"html_url"` }

关于字段名称,即使对应的JSON字段的名称都是小写的,但是结构体中的字段必须首字母大写(不可导出的字段也无法把JSON数据导入)。这种情况很普遍,这里可以偷个懒。在 Unmarshal 阶段,JSON字段的名称关联到Go结构体成员的名称是忽略大小写的,这里也不需要考虑序列化的问题,所以很多地方都不需要写成员标签。不过,小写的变量在需要分词的时候,可能会使用下划线分割,这种情况下,还是要用一下成员标签的。
这里也是选择性地对JSON中的字段进行解码,因为相对于这里演示的内容,GitHub的查询返回的信息是相当多的。
请求获取JSON并解析 函数 SearchIssues 发送HTTP请求并将返回的JSON字符串进行解析。
关于Get请求的参数,参数中可能会出现URL格式里的特殊字符,比如 ?、&。因此要使用 url.QueryEscape 函数进行转义。
// ch4/github/search.go package 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 中的内容进行格式化输出,这里用了固定宽度的方法将结果输出为类似表格的形式:
// ch4/issues/main.go // 将符合条件的issue输出为一个表格 package mainimport ( "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 #19109bradfitz 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) }

模板输出的过程 通过模板输出结果需要两个步骤:
  1. 解析模板并转换为内部表示的方法
  2. 在指定的输入上执行(就是执行并输出)
解析模板只需要执行一次。下面的代码创建并解析上面定义的文本模板:
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/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) } }

这个版本还可以改善,下面对解析错误的处理进行了改进
帮助函数 Must 由于目标通常是在编译期间就固定下来的,因此无法解析将会是一个严重的bug。上面的版本如果无法解析(去掉个大括号试试),只会以比较温和的方式报告出来。
这里推荐使用帮助函数 template.Must,模板错误会Panic:
package mainimport ( "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 模板 接着看 html/template 包。它使用和 text/template 包里一样的 API 和表达式语法,并且额外地对出现在 HTML、JavaScript、CSS 和 URL 中的字符串进行自动转义。这样可以避免在生成 HTML 是引发一些安全问题。
使用模板输出页面 下面是一个将 issue 输出为 HTML 表格代码。由于两个包里的API是一样的,所以除了模板本身以外,GO代码没有太大的差别:
package mainimport ( "fmt" "log" "net/http" "os" )import ( "gopl/ch4/github" "html/template" )var issueList = template.Must(template.New("issuelist").Parse(` {{.TotalCount}} issues {{range .Items}} {{end}}
# State User Title
{{.Number}} {{.State}} {{.User.Login}} {{.Title}}
`))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 = `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)) }

【JSON、文本模板、HTML模板】转载于:https://blog.51cto.com/steed/2353945

    推荐阅读