JSON、文本模板、HTML模板

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 跟踪接口来演示一下。

定义结构体

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

// 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)
}

模板输出的过程

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

  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 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))
}

原文地址:https://blog.51cto.com/steed/2353945

时间: 2024-08-01 03:01:42

JSON、文本模板、HTML模板的相关文章

ajax请求后台,返回json格式数据,模板!

添加一个用户的时候,需要找出公司下所有的部门,和相应部门下的角色,利用ajax请求,实现联动技术.将返回的json格式数据,添加到select标签下. <script type="text/javascript">        //加载出部门的信息            function loadGroup(){                            $.ajax({                    type:"post",  

Flask Web Development - Flask 模板1 - 模板机制&Jinja2引擎

节选自PartI Chapter3,这个chapter主要讲模板工作原理,这里讲的就是Jinja2这个模板,另外还提到了Flask-Bootstrap及Flask-Moment两个插件,前者对Flask使用Bootstrap做了些封装,后者对moment.js做了些封装.内容较多,估计分开搞. 模板存在的意义 可维护性高的代码是结构良好且整洁的. 当用户在网站注册一个账户时,他在表单里填入邮箱跟密码,并点击提交按钮.在server端就收到一个包含这些数据的request,再由Flask分发到相应

模板系列(一) 模板的模板参数

前面我们写过类似的Stack: template <typename T, typename Alloc = std::vector<T> > class Stack { public: void push(const T &); void pop(); T top() const; bool empty() const; private: Alloc _cont; }; 那么我们使用的时候需要这样: Stack<string, list<string>

畅通工程续 (SPFA模板Floy模板)

http://acm.hdu.edu.cn/showproblem.php?pid=1874 SPFA #include <iostream> #include <stdio.h> #include <string.h> #include <stdlib.h> #define N 1000001 using namespace std; int n,m; int v[202],dis[202]; struct node { int x,y,z,next; }

Struts2中使用Velocity模板时模板资源路径配置问题

在Struts2中使用Velocity模板时,如何以相对与Web工程的路径来配置模板资源文件路径这个问题网上千篇一律的来自Velocity官方文档.官方文档中指出如果是Web工程的话,模板的相对路径是工程根路径,今天在使用的时候有如下配置: Velocity.properties(默认在WEB-INF下): resource.loader =file, classclass.resource.loader.description = Velocity Classpath Resource Loa

JSON文本转换为JSONArray 转换为 List&lt;Object&gt;

1 package com.beijxing.TestMain; 2 3 import java.io.File; 4 import java.io.IOException; 5 import java.util.ArrayList; 6 import java.util.List; 7 8 import org.apache.commons.io.FileUtils; 9 10 import com.beijxing.entity.Student; 11 12 import net.sf.js

json文本装换为JSONArray

1 package com.beijxing.TestMain; 2 3 import java.io.File; 4 import java.io.IOException; 5 6 import org.apache.commons.io.FileUtils; 7 8 import net.sf.json.JSONArray; 9 import net.sf.json.JSONObject; 10 11 /** 12 * JSON文本转换为JSONArray 13 * @author 作者 :

C++提高1 【泛型编程】函数模板 类模板

[本文谢绝转载] [泛型编程] 函数模板 为什么会有函数模板 现象: 函数的业务逻辑一样 函数的参数类型不一样 [最常用]函数模板  显式的调用 [不常用]类型推导 多个参数,参数定义了必须要用 函数模板,实现int类型数组,char字符串排序: 函数模板 与 普通函数的本质区别 函数模板 和 普通函数在一起 的调用型研究: C++是如何支持函数模板机制的? 函数模板机制结论 类模板 类模板的定义 类模板做函数的参数 类模板的派生成普通类 模板类的派生成模板类 复数类,所有函数都写在类的内部,运

C++:类模板与模板类

6.3 类模板和模板类 所谓类模板,实际上是建立一个通用类,其数据成员.成员函数的返回值类型和形参类型不具体指定,用一个虚拟的类型来代表.使用类模板定义对象时,系统会实参的类型来取代类模板中虚拟类型从而实现了不同类的功能. 定义一个类模板与定义函数模板的格式类似,必须以关键字template开始,后面是尖括号括起来的模板参数,然后是类名,其格式如下: template <typename 类型参数> class 类名{       类成员声明 }; 或者 template <class

函数模板 类模板

摘要:学习函数模板的定义,使用:学习类模板的定义和使用. 函数模板: template <typename 类型参数> 返回类型 函数名(模板形参表) { 函数体 } 特点:1.函数模板可以重载(比如形参数量不同的情况). 2.定义的时候,template <typename 类型参数>到下面一个语句之间不允许插入其他语句! 3.如果程序中有和函数模板名称相同的非函数模板函数,则优先调用它. 例子: #include<iostream> using namespace