JSON、文本模板、HTML模板
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 跟蹤接口來演示一下。
定義結構體
首先,定義好類型,順便還有常量:
// 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的查詢返回的信息是相當多的。
請求獲取JSON並解析
函數 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)
}
}
這個版本還可以改善,下面對解析錯誤的處理進行了改進
幫助函數 Must
由於目標通常是在編譯期間就固定下來的,因此無法解析將會是一個嚴重的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 模板
接著看 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(`
<h1>{{.TotalCount}} issues</h1>
<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 main
import (
"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))
}
JSON、文本模板、HTML模板