复合数据类型(构造类型)
JSON
golang 天生支持 JSON 和 HTML,意味着它天生为网络编程服务。
JSON使用的基本类型是数字(十进制或科学计数法)、布尔值(true或false)、字符串(Unicode码点序列,用\为转义符,\uhhh得到的是UTF-16字符)。
JSON用基本类型可以组合出构造类型:数组和对象。JSON的数组和golang数组和slice对应,JSON的对象和golang map和结构体对应。把golang的数组或结构体之类转成JSON的过程,称为Marshal(列集),而把JSON数据转换到golang数组或结构体变量,称为Unmarshal(反列集,解码列集)。因为golang结构体等用首字母大小写控制访问,而JSON字段名完全可能小写开头,而且,我们有时候并不想JSON字段名和结构体成员名一致,这时需要字段标签(field tag),它是编译期给编译器提示用的元信息。
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
var movies = []Movie{
{Title: "T1", Year: 1942, Color: false, Actor: []string{"AA", "BB"}},
{Title: "T2", Year: 1967, Color: true, Actor: []string{"CC", "DD"}},
{Title: "T3", Year: 1968, Color: true, Actor: []string{"EE", "FF"}},
}
data, err := json.MarshalIndent(movies, "", " ")
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles)
}
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actor []string
}
输出
[
{
"Title": "T1",
"released": 1942,
"Actor": [
"AA",
"BB"
]
},
{
"Title": "T2",
"released": 1967,
"color": true,
"Actor": [
"CC",
"DD"
]
},
{
"Title": "T3",
"released": 1968,
"color": true,
"Actor": [
"EE",
"FF"
]
}
]
PS C:\Users\zime\go\src\ch1\hello> go run .
[
{
"Title": "T1",
"released": 1942,
"Actor": [
"AA",
"BB"
]
},
{
"Title": "T2",
"released": 1967,
"color": true,
"Actor": [
"CC",
"DD"
]
},
{
"Title": "T3",
"released": 1968,
"color": true,
"Actor": [
"EE",
"FF"
]
}
]
[{T1} {T2} {T3}]
字段标签(总是反引号包围) json:"color,omitempty" ,表示对应JSON字段为color,并且该字段为零值(对bool型false就是零值)时从JSON忽略它,所以,对于Color为false的条目,JSON中没有color项。
下面的例子是向GitHub发送Get请求获取JSON格式的issue有关信息。创建项目目录github,go mod init github 添加依赖跟踪文件go.mod,然后在项目目录github下创建data.go和func.go 。
data.go 文件定义了一些结构体,对应返回的JSON格式的数据(只对应需要提取的信息,因为原始返回数据字段很多)
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
CreatedAt time.Time `json:"created_at"`
Body string // in markdown format
}
type User struct {
Login string
HtmlUrl string `json:"html_url"`
}
func.go 包含了发起请求进行查询的函数(请求类似 https://api.github.com/search/issues?q=repo%3Agolang%2Fgo+is%3Aopen+json+decoder)
package github
import (
"fmt"
// "io/ioutil"
"encoding/json"
"net/http"
"net/url"
"strings"
)
func SearchIssues(terms []string) (*IssuesSearchResult, error) {
q := url.QueryEscape(strings.Join(terms, " ")) // 注意,一个空格连接各部分
println(IssuesUrl + "?q=" + q)
resp, err := http.Get(IssuesUrl + "?q=" + q)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("查询失败: %s", resp.Status)
}
var result IssuesSearchResult
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
resp.Body.Close()
return nil, err
}
// info, _ := ioutil.ReadAll(resp.Body)
// fmt.Println(string(info)) // 查看原始返回信息
resp.Body.Close()
return &result, nil
}
另外创建一个项目,目录 src/issues,该项目将调用前述github项目中的函数。go mod init issues 创建依赖跟踪文件 go.mod。用 go mod edit -replace github=../github 对依赖项 github 进行重定向(参考另一贴 golang学习随便记1-准备工作、程序结构_sjg20010414的博客-CSDN博客),然后 go mod tidy 添加依赖项。该项目下文件 issues.go 如下
package main
import (
"fmt"
"github"
"log"
"os"
)
// test like: go run . repo:golang/go is:open json decoder
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d 个问题:\n", result.TotalCount)
for _, item := range result.Items {
fmt.Printf("#%-5d %9.9s %.55s\n", item.Number, item.User.Login, item.Title)
}
}
输出如下:(省略后面类似的行)
sjg@sjg-PC:~/go/src/issues$ go run . repo:golang/go is:open json decoder
https://api.github.com/search/issues?q=repo%3Agolang%2Fgo+is%3Aopen+json+decoder
79 个问题:
#56733 rolandsho encoding/json: add (*Decoder).SetLimit
#48298 dsnet encoding/json: add Decoder.DisallowDuplicateFields
#59053 joerdav proposal: encoding/json: add a generic Decode function
#29035 jaswdr proposal: encoding/json: add error var to compare the
#36225 dsnet encoding/json: the Decoder.Decode API lends itself to m
#42571 dsnet encoding/json: clarify Decoder.InputOffset semantics
文本和HTML模板
接着上述 json 例子,我们把请求返回的json,用指定的文本模板输出。
新建项目目录 src/issuesreport,go mod init issuesreport,因为我们要用到 github 项目包,所以,go mod edit -replace github=../github 添加重定向,编写以下代码issuesreport.go并用go mod tidy 添加依赖:
package main
import (
"github"
"log"
"os"
"text/template"
"time"
)
const tpl = `{{.TotalCount}} 个问题:
{{range .Items}}-------------------
编号: {{.Number}}
用户: {{.User.Login}}
标题: {{.Title | printf "%.64s"}}
时长: {{.CreatedAt | daysAgo}} 天
{{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(tpl))
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.New 创建指定名称的模板对象,模板对象的方法Funcs添加模板中的函数键名到实际函数的映射关系,模板对象的方法Parse将解析模板(Funcs必须在Parse前面),准备调用模板对象的Execute方法“执行模板”。所谓执行模板,就是将数据合并到模板,然后输出。
输出结果类似如下:(省略后续类似的项)
sjg@sjg-PC:~/go/src/issuesreport$ go run . repo:golang/go is:open json decoder
https://api.github.com/search/issues?q=repo%3Agolang%2Fgo+is%3Aopen+json+decoder
79 个问题:
-------------------
编号: 56733
用户: rolandshoemaker
标题: encoding/json: add (*Decoder).SetLimit
时长: 163 天
-------------------
编号: 48298
用户: dsnet
标题: encoding/json: add Decoder.DisallowDuplicateFields
时长: 594 天
-------------------
Html模板的使用和文本模板并无多少差别:mkdir issueshtml 创建项目目录,cd issueshtml,go mod init issueshtml 初始化,同样,用 go mod edit -replace github=../github 使用本地包,将项目目录添加到VS Code的工作区,项目目录下新建 issueshtml.go,添加如下代码 (注意:前面 github项目中 data.go 中,我们定义的关于HtmlURL的字段名都是HtmlUrl,模板中使用时大小写务必与此一致)
package main
import (
"github"
"html/template"
"log"
"os"
)
var issueList = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} 个问题</h1>
<table>
<tr style='text-align: left'>
<th>#</th>
<th>状态</th>
<th>用户</th>
<th>主题</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><a href='{{.HtmlUrl}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
if err := issueList.Execute(os.Stdout, result); err != nil {
log.Fatal(err)
}
}
然后, go mod tidy 在 go.mod 中添加上代码中的依赖 github包。运行 go run . repo:golang/go commenter:gopherbot json encoder >issues.html 将输出结果重定向到html文件。打开该html文件,结果类似如下图
表面上看,html/template 和 text/template 没有差别,但区别是,html/template 会自动对特殊字符(如 < 、>符号)转义,而 text/template 不转义。与此同时,存在一种需求,即 html/template 模板中需要对某些字符串按原始样子输出(不转义)。我们可以使用类型 template.HTML 来标记可信HTML字符串的类型。下面的例子可以直观展示这一点: autoescaple.go 代码如下
package main
import (
"html/template"
"log"
"os"
)
func main() {
const tpl = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
t := template.Must(template.New("escape").Parse(tpl))
var data struct {
A string
B template.HTML // 可信 HTML
}
data.A = "<b>Hello!</b>"
data.B = "<b>Hello!</b>"
if err := t.Execute(os.Stdout, data); err != nil {
log.Fatal(err)
}
}
go run . 输出结果为:<p>A: <b>Hello!</b></p><p>B: <b>Hello!</b></p>