Go-网络编程-四-

81 阅读27分钟

Go 网络编程(四)

原文:Network programming with Go

协议:CC BY-NC-SA 4.0

九、模板

大多数服务器端语言都有一种机制,主要是获取静态页面并插入动态生成的组件,比如一个项目列表。典型的例子是 Java 服务器页面中的脚本、PHP 脚本和许多其他脚本。Go 在template包中采用了相对简单的脚本语言。

该包设计为将文本作为输入,并基于使用对象的值转换原始文本来输出不同的文本。不像 JSP 或类似的,它并不局限于 HTML 文件,但它很可能在那里找到最大的用途。我们首先描述text/template包,然后描述html/template包。

原始源称为模板,将由不变地传输的文本和可以作用于并改变文本的嵌入命令组成。命令由{{ ... }}分隔,类似于 JSP 命令<%= ... =%>和 PHP 的<?php ... ?>

插入对象值

模板应用于 Go 对象。来自 Go 对象的字段可以插入到模板中,您可以“挖掘”对象以找到子字段,等等。当前对象表示为光标.,因此要将当前对象的值作为字符串插入,可以使用{{.}}。默认情况下,这个包使用fmt包来计算作为插入值的字符串。

要插入当前光标对象的某个字段的值,可以使用前缀为.的字段名称。例如,如果当前光标对象的类型为

type Person struct {
        Name      string
        Age       int
        Emails     []string
        Jobs       []*Job
}

如下插入NameAge的值:

The name is {{.Name}}.
The age is {{.Age}}.

您可以使用range命令遍历数组或其他列表的元素。因此,要访问Emails数组的内容,您可以使用:

{{range .Emails}}
        The email is {{.}}

{{end}}

在邮件循环期间,光标.被依次设置到每封邮件。循环结束时,光标返回到人。如果Job定义如下:

type Job struct {
    Employer string
    Role     string
}

并且我们想要访问一个personjobs的字段,我们可以像上面一样用一个{{range .Jobs}}来完成。另一种方法是将当前对象切换到Jobs字段。这是通过使用{{with ...}} ... {{end}}构造完成的,其中{{.}}Jobs字段,这是一个数组:

{{with .Jobs}}
    {{range .}}
        An employer is {{.Employer}}
        and the role is {{.Role}}
    {{end}}
{{end}}

您可以将它用于任何字段,而不仅仅是数组。

使用模板

一旦你有了一个模板,你就可以把它应用到一个对象来生成一个新的字符串,用这个对象来填充模板值。这是一个两步过程,包括解析模板,然后将其应用于对象。结果被输出到一个Writer,如:

t := template.New("Person template")
t, err := t.Parse(templ)
if err == nil {
        buff := bytes.NewBufferString("")
        t.Execute(buff, person)
}

将模板应用到对象并打印到标准输出的示例程序是PrintPerson.go:

/**
 * PrintPerson
 */

package main

import (
        "fmt"
        "text/template"
        "os"
)

type Person struct {
        Name   string
        Age    int
        Emails []string

        Jobs   []*Job
}

type Job struct {
        Employer string

        Role     string
}

const templ = `The name is {{.Name}}.
The age is {{.Age}}.
{{range .Emails}}
        An email is {{.}}
{{end}}

{{with .Jobs}}
    {{range .}}
        An employer is {{.Employer}}
        and the role is {{.Role}}
    {{end}}
{{end}}
`

func main() {
        job1 := Job{Employer: "Box Hill Institute", Role: "Director, Commerce and ICT"}
        job2 := Job{Employer: "Canberra University", Role: "Adjunct Professor"}

        person := Person{
                Name:   "jan",
                Age:    66,
                Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
                Jobs:   []*Job{&job1, &job2},
        }

        t := template.New("Person template")
        t, err := t.Parse(templ)
        checkError(err)

        err = t.Execute(os.Stdout, person)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

其输出如下所示:

The name is jan.
The age is 66.

        An email is jan@newmarch.name

        An email is jan.newmarch@gmail.com

        An employer is Canberra University
        and the role is Adjunct Professor

        An employer is Box Hill Institute
        and the role is Director, Commerce and ICT

注意,在这个打印输出中有大量的空白作为换行符。这是因为我们在模板中有空白。如果您想减少这个空白,请删除模板中的换行符,如下所示:

{{range .Emails}} An email is {{.}} {{end}}

另一种方法是使用命令分隔符"{{- " and " -}}"分别从紧接的前一个文本中删除所有尾随空白,从紧接的后一个文本中删除所有前导空白。

在这个例子中,我们在程序中使用了一个字符串作为模板。您也可以使用template.ParseFiles()功能从文件中加载模板。由于某种我不理解的原因(在早期版本中不需要),分配给模板的名称必须与文件列表中第一个文件的基本名称相同。这是个 bug 吗?

管道

上述转换将文本片段插入到模板中。这些文本基本上是任意的,不管字段的字符串值是什么。如果我们希望它们作为 HTML 文档(或其他特殊形式)的一部分出现,我们必须对特定的字符序列进行转义。例如,要在 HTML 文档中显示任意文本,我们必须将<改为&lt;。Go 模板有许多内置函数,其中之一就是html()。这些函数以类似于 UNIX 管道的方式工作,从标准输入读取数据并写入标准输出。

要获取当前对象.的值并对其应用 HTML 转义,您需要在模板中编写一个“管道”:

{{. | html}}

对于其他功能也是如此。

定义函数

模板使用对象的字符串表示来插入值,使用fmt包将对象转换成字符串。有时候这并不是我们所需要的。例如,为了避免垃圾邮件发送者获得电子邮件地址,很常见的是将符号@替换为单词“at”,如“jan at newmarch.name”。如果我们想使用模板以那种形式显示电子邮件地址,我们必须构建一个自定义函数来完成这种转换。

每个模板函数都有一个用于模板本身的名称和一个关联的 Go 函数。这些由以下类型链接:

type FuncMap map[string]interface{}

例如,如果我们希望我们的模板函数是emailExpand,它链接到 Go 函数EmailExpander,我们将它添加到模板中的函数,如下所示:

t = t.Funcs(template.FuncMap{"emailExpand": EmailExpander})

EmailExpander的签名通常如下:

func EmailExpander(args ...interface{}) string

对于我们感兴趣的用法,函数应该只有一个参数,它将是一个字符串。Go 模板库中的现有函数有一些初始代码来处理不一致的情况,所以我们只是复制这些代码。然后,只需简单的字符串操作就可以改变电子邮件地址的格式。一个程序是PrintEmails.go:

/**
 * PrintEmails
 */

package main

import (
        "fmt"
        "os"
        "strings"
        "text/template"
)

type Person struct {
        Name   string
        Emails []string
}

const templ = `The name is {{.Name}}.
{{range .Emails}}
        An email is "{{. | emailExpand}}"
{{end}}
`

func EmailExpander(args ...interface{}) string {

        ok := false
        var s string
        if len(args) == 1 {
                s, ok = args[0].(string)
        }
        if !ok {
                s = fmt.Sprint(args...)
        }

        // find the @ symbol
        substrs := strings.Split(s, "@")
        if len(substrs) != 2 {
                return s
        }
        // replace the @ by " at "
        return (substrs[0] + " at " + substrs[1])
}

func main() {
        person := Person{
                Name:   "jan",
                Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        }

        t := template.New("Person template")

        // add our function
        t = t.Funcs(template.FuncMap{"emailExpand": EmailExpander})

        t, err := t.Parse(templ)

        checkError(err)

        err = t.Execute(os.Stdout, person)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

输出如下所示:

The name is jan.

        An email is "jan at newmarch.name"

        An email is "jan.newmarch at gmail.com"

变量

模板包允许您定义和使用变量。作为这样做的动机,考虑我们如何打印每个人的电子邮件地址,并以他们的名字作为前缀。我们使用的类型也是这个:

type Person struct {
        Name      string
        Emails     []string
}

为了访问电子邮件字符串,我们使用如下的range语句:

{{range .Emails}}
    {{.}}
{{end}}

但是此时我们不能访问Name字段,因为.正在遍历数组元素,而Name不在这个范围内。解决方案是将Name字段的值保存在一个变量中,该变量在其作用域内的任何地方都可以被访问。模板中的变量以$为前缀。所以我们这样写:

{{$name := .Name}}
{{range .Emails}}
    Name is {{$name}}, email is {{.}}
{{end}}

程序是PrintNameEmails.go:

/**
 * PrintNameEmails
 */

package main

import (
        "text/template"
        "os"
        "fmt"
)

type Person struct {
        Name   string
        Emails []string
}

const templ = `{{$name := .Name}}
{{range .Emails}}
    Name is {{$name}}, email is {{.}}

{{end}}
`

func main() {
        person := Person{
                Name:   "jan",
                Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        }

        t := template.New("Person template")
        t, err := t.Parse(templ)
        checkError(err)

        err = t.Execute(os.Stdout, person)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

以下是输出:

Name is jan, email is jan@newmarch.name

Name is jan, email is jan.newmarch@gmail.com

条件语句

继续Person的例子,假设您只想打印出电子邮件列表,而不想深入研究它。您可以使用模板来做到这一点:

Name is {{.Name}}
Emails are {{.Emails}}

这将打印以下内容:

Name is jan
Emails are [jan@newmarch.name jan.newmarch@gmail.com]

因为这是fmt包显示列表的方式。

在许多情况下,这可能是好的,如果这是你想要的。让我们考虑一个几乎正确,但不完全正确的情况。有一个 JSON 包来序列化对象,我们在第四章中讨论过。这将产生以下结果:

{"Name": "jan",
 "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com"]
}

JSON 包是您在实践中使用的包,但是让我们看看是否可以使用模板生成 JSON 输出。我们可以通过现有的模板做类似的事情。作为 JSON 序列化程序,这几乎是正确的:

{"Name": "{{.Name}}",
 "Emails": {{.Emails}}

}

它会产生这样的结果:

{"Name": "jan",
 "Emails": [jan@newmarch.name jan.newmarch@gmail.com]
}

这有两个问题:地址没有用引号括起来,列表元素应该用,分隔。

这样如何——看看数组元素,把它们放在引号中,然后加上逗号?

{"Name": {{.Name}},
  "Emails": [
   {{range .Emails}}
      "{{.}}",
   {{end}}
  ]
}

这将产生:

{"Name": "jan",
 "Emails": ["jan@newmarch.name", "jan.newmarch@gmail.com",]
}

(加上一些空格。)

同样,这几乎是正确的,但是如果仔细观察,您会看到在最后一个列表元素后面有一个尾随的,。根据 JSON 语法(参见 http://www.json.org/ ),这种尾随的,是不允许的。实现可能在处理这一问题的方式上有所不同。

我们想要的是打印除了最后一个元素之外的每个元素,后跟一个,。这实际上有点难做到,所以更好的方法是打印每个以 a 开头的元素,除了第一个。(这个技巧是我在栈溢出— http://stackoverflow.com/questions/201782/can-you-use-a-trailing-comma-in-a-json-object 从“brianb”那里得到的)。这更容易,因为第一个元素的索引是零,许多编程语言,包括 Go 模板语言,都将零视为布尔值false

条件语句的一种形式是{{if pipeline}} T1 {{else}} T0 {{end}}。我们需要将pipeline作为电子邮件数组的索引。幸运的是,range声明的一个变体给了我们这个。有两种引入变量的形式:

{{range $elmt := array}}
{{range $index, $elmt := array}}

所以我们通过数组建立了一个循环,如果索引是false (0),我们就打印这个元素。否则,我们会在它前面打印一个,。模板如下所示:

{"Name": "{{.Name}}",
 "Emails": [
 {{range $index, $elmt := .Emails}}
    {{if $index}}
        , "{{$elmt}}"
    {{else}}
         "{{$elmt}}"
    {{end}}
 {{end}}
 ]
}

完整的程序是PrintJSONEmails.go:

/**
 * PrintJSONEmails
 */

package main

import (
        "text/template"
        "os"
        "fmt"
)

type Person struct {
        Name   string
        Emails []string
}

const templ = `{"Name": "{{.Name}}",
 "Emails": [
{{range $index, $elmt := .Emails}}
    {{if $index}}
        , "{{$elmt}}"
    {{else}}
         "{{$elmt}}"
    {{end}}
{{end}}
 ]
}
`

func main() {
        person := Person{
                Name:   "jan",
                Emails: []string{"jan@newmarch.name", "jan.newmarch@gmail.com"},
        }

        t := template.New("Person template")

        t, err := t.Parse(templ)
        checkError(err)

        err = t.Execute(os.Stdout, person)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

这给出了正确的 JSON 输出。

在离开本节之前,请注意使用逗号分隔符格式化列表的问题可以通过在 Go 中定义合适的函数来解决,这些函数可以作为模板函数使用。借用另一种编程语言中的一句名言,“有不止一种方法可以做到这一点!”。以下程序是罗杰·佩佩以Sequence.go的身份发给我的:

/**
 * Sequence.go
 * Copyright Roger Peppe
 */

package main

import (
        "errors"
        "fmt"
        "os"
        "text/template"
)

var tmpl = `{{$comma := sequence "" ", "}}
{{range $}}{{$comma.Next}}{{.}}{{end}}
{{$comma := sequence "" ", "}}
{{$colour := cycle "black" "white" "red"}}
{{range $}}{{$comma.Next}}{{.}} in {{$colour.Next}}{{end}}
`

var fmap = template.FuncMap{
        "sequence": sequenceFunc,
        "cycle":    cycleFunc,
}

func main() {
        t, err := template.New("").Funcs(fmap).Parse(tmpl)
        if err != nil {
                fmt.Printf("parse error: %v\n", err)

                return
        }
        err = t.Execute(os.Stdout, []string{"a", "b", "c", "d", "e", "f"})
        if err != nil {
                fmt.Printf("exec error: %v\n", err)
        }
}

type generator struct {
        ss []string
        i  int
        f  func(s []string, i int) string
}

func (seq *generator) Next() string {
        s := seq.f(seq.ss, seq.i)
        seq.i++
        return s
}

func sequenceGen(ss []string, i int) string {
        if i >= len(ss) {
                return ss[len(ss)-1]
        }
        return ss[i]
}

func cycleGen(ss []string, i int) string {
        return ss[i%len(ss)]
}

func sequenceFunc(ss ...string) (*generator, error) {
        if len(ss) == 0 {
                return nil, errors.New("sequence must have at least one element")
        }
        return &generator{ss, 0, sequenceGen}, nil
}

func cycleFunc(ss ...string) (*generator, error) {
        if len(ss) == 0 {
                return nil, errors.New("cycle must have at least one element")
        }
        return &generator{ss, 0, cycleGen}, nil
}

以下是输出:

a, b, c, d, e, f

a in black, b in white, c in red, d in black, e in white, f in red

HTML/模板包

前面的程序都处理了text/template包。这将应用转换,而不考虑可能使用文本的任何上下文。例如,如果PrintPerson.go中的文本变为:

job1 := Job{Employer: "<script>alert('Could be nasty!')</script>", Role: "Director, Commerce and ICT"}

该程序将生成以下文本:

An employer is <script>alert('Could be nasty!')</script>

如果下载到浏览器中,将会产生意想不到的效果。

在管道中使用html命令可以减少这种情况,如{{。| html}},并将产生以下内容:

An employer is &lt;script&gt;alert('Could be nasty!')&lt;/script&gt

将此过滤器应用于所有表达式将变得繁琐。此外,它可能无法捕捉潜在危险的 JavaScript、CSS 或 URI 表达式。

html/template包就是为了克服这些问题而设计的。通过用html/template替换text/template的简单步骤,适当的转换将被应用到结果文本,净化它,使它适合 web 上下文。

结论

Go 模板包对于某些涉及插入对象值的文本转换非常有用。例如,它不具备正则表达式的能力,但它比正则表达式更快,在许多情况下也更容易使用。

十、完整的网络服务器

这一章主要是对 HTTP 一章的说明,在 Go 中构建一个完整的 Web 服务器。它还展示了如何使用模板,以便在文本文件中使用表达式来插入变量值和生成重复的部分。它处理序列化数据和 Unicode 字符集。本章中的程序足够长且复杂,所以它们并不总是完整地给出,但可以从本书的网站上下载,该网站是 http://www.apress.com/9781484226919

我正在学习中文。相反,经过多年的努力,我仍在尝试学习中文。当然,我没有埋头苦干,而是尝试了各种技术辅助手段。我尝试了教科书、视频和许多其他教具。最终我意识到我进步缓慢的原因是没有一个好的中文抽认卡的计算机程序,所以为了学习,我需要建立一个。

我在 Python 中找到了一个程序来完成一些任务。但遗憾的是,它写得不好,在几次试图把它颠倒过来之后,我得出的结论是,最好从零开始。当然,一个网络解决方案要比一个独立的解决方案好得多,因为这样我的中文课上的其他人就可以分享它,还有其他的学习者。当然,服务器应该是用 Go 编写的。

我使用了张芃芃《汉语口语精读》一书中的词汇,但是这个程序适用于任何词汇集。

浏览器站点图

在浏览器中看到的结果程序有三种类型的页面,如图 10-1 所示。

A436770_1_En_10_Fig1_HTML.gif

图 10-1。

Browser pages

主页显示抽认卡组列表(见图 10-2 )。它包括当前可用的抽认卡组列表、您希望抽认卡组如何显示(随机卡顺序、首先显示中文或英文,或随机),以及是显示一组卡还是只显示一组卡中的单词。

A436770_1_En_10_Fig2_HTML.jpg

图 10-2。

The home page of the web site

抽认卡组显示一张抽认卡,一次一张。一个看起来像图 10-3 。

A436770_1_En_10_Fig3_HTML.jpg

图 10-3。

Typical flashcard showing all the components

抽认卡的单词集如图 10-4 所示。

A436770_1_En_10_Fig4_HTML.jpg

图 10-4。

The list of words in a flashcard set

浏览器文件

浏览器端有 HTML,CSS,JavaScript 文件。这些措施如下:

  • 首页(flashcards.html):
    • html/ListFlashcardsStylesheet.css
  • 抽认卡组(ShowFlashcards.html):
    • css/CardStyleSheet.css
    • jscript/jquery.js
    • jscript/slideviewer.js
  • 抽认卡设置单词(ListWords.html):无额外

基本服务器

该服务器是一个 HTTP 服务器,如前一章所述。它有许多功能来处理不同的网址。这些功能概述如下:

| 小路 | 功能 | HTML 已交付 | | --- | --- | --- | | `/` | `listFlashCards` | `html/ListFlashcards.html` | | `/flashcards.html` | `listFlashCards` | `html/ListFlashcards.html` | | `/flashcardSets` | `manageFlashCards` | `html/showFlashcards.html` | | `/flashcardSets` | `manageFlashCards` | `html/ListWords.html` | | `/jscript/*` | `fileServer` | 目录`/jscript`中的文件 | | `/html/*` | `fileServer` | 目录`/html`中的文件 |

暂且省略功能本身,服务器是 http://www.apress.com/9781484226919Ch10下的Server.go

/* Server
 */

package main

import (
        "fmt"
        "net/http"
        "os"
        "html/template"
)

import (
        "dictionary"
        "flashcards"
        "templatefuncs"
)

func main() {
       if len(os.Args) != 2 {
                fmt.Fprint(os.Stderr, "Usage: ", os.Args[0], ":port\n")
                os.Exit(1)
       }
        port := os.Args[1]

        http.HandleFunc("/", listFlashCards)
        fileServer := http.StripPrefix("/jscript/", http.FileServer(http.Dir("jscript")))
        http.Handle("/jscript/", fileServer)
        fileServer = http.StripPrefix("/html/", http.FileServer(http.Dir("html")))
        http.Handle("/html/", fileServer)

        http.HandleFunc("/flashcards.html", listFlashCards)
        http.HandleFunc("/flashcardSets", manageFlashCards)

        // deliver requests to the handlers
        err := http.ListenAndServe(port, nil)
        checkError(err)
        // That's it!

}

func listFlashCards(rw http.ResponseWriter, req *http.Request) {
         ...
}

/*
 * Called from ListFlashcards.html on form submission
 */
func manageFlashCards(rw http.ResponseWriter, req *http.Request) {
        ...
}

func showFlashCards(rw http.ResponseWriter, cardname, order, half string) {
        ...
}

func listWords(rw http.ResponseWriter, cardname string) {
         ...
}

func checkError(err error) {
        if err != nil {
                  fmt.Println("Fatal error ", err.Error())
                  os.Exit(1)
        }
}

我们现在开始讨论单个函数。

listFlashCards 函数

调用listFlashCards函数为顶层页面创建 HTML。抽认卡名称列表是可扩展的,是目录flashcardSets中的一组文件条目。此列表用于在顶级页面中创建表格,最好使用模板包来完成:

<table>
  {{range .}}
  <tr>
    <td>
      {{.}}
    </td>
  </tr>
</table>

其中范围超出了名称列表。文件html/ListFlashcards.html包含这个模板以及卡片顺序、半卡片显示和底部表单按钮的 HTML。省略了边列表和提交按钮,HTML 如下:

<html>
  <head>
    <title>
      Flashcards
    </title>
    <link type="text/css" rel="stylesheet"
          href="/html/ListFlashcardsStylesheet.css">
    </link>
  </head>
  <body>
    <h1>
      Flashcards
    </h1>
    <p>

      <div id="choose">
        <form method="GET" action="http:flashcardSets">

          <table border="1" id="sets">
            <tr>
              <th  colspan="2">
                Flashcard Sets
              </th>
            </tr>
            {{range .}}
            <tr>
              <td>
                {{.}}
              </td>
              <td>
                <input type="radio" name="flashcardSets" value="{{.}}" />
              </td>
            </tr>
            {{end}}
          </table>
       </div>
    </p>
  </body>
</html>

将模板应用于此的函数listFlashCards如下:

func listFlashCards(rw http.ResponseWriter, req *http.Request) {

         flashCardsNames := flashcards.ListFlashCardsNames()
         t, err := template.ParseFiles("html/ListFlashcards.html")
         if err != nil {
                   http.Error(rw, err.Error(), http.StatusInternalServerError)
                   return
         }
         t.Execute(rw, flashCardsNames)
}

函数flashcards.ListFlashCardsNames()只是遍历抽认卡目录,返回一个字符串数组(每个抽认卡集的文件名):

func ListFlashCardsNames() []string {
        flashcardsDir, err := os.Open("flashcardSets")
        if err != nil {
                return nil
        }
        files, err := flashcardsDir.Readdir(-1)

        fileNames := make([]string, len(files))
        for n, f := range files {
                fileNames[n] = f.Name()
        }
        sort.Strings(fileNames)
        return fileNames
}  

manageFlashCards 功能

按下“显示集合中的卡片”按钮或“列出集合中的单词”按钮时,调用manageFlashCards函数来管理表单提交。它从表单请求中提取值,然后在showFlashCardslistWords之间进行选择:

func manageFlashCards(rw http.ResponseWriter, req *http.Request) {

       set := req.FormValue("flashcardSets")
       order := req.FormValue("order")
       action := req.FormValue("submit")
       half := req.FormValue("half")
       cardname := "flashcardSets/" + set

       fmt.Println("cardname", cardname, "action", action)
       if action == "Show cards in set" {
                 showFlashCards(rw, cardname, order, half)
       } else if action == "List words in set" {
                 listWords(rw, cardname)
       }
}

汉语词典

前面的代码相当普通:它使用文件服务器交付静态文件,使用基于目录中文件列表的模板创建 HTML 表,并处理来自 HTML 表单的信息。为了进一步了解每张卡片上显示的内容,我们必须了解应用程序的具体细节,这意味着要了解单词的来源(字典),如何表示单词和卡片,以及如何将抽认卡数据发送到浏览器。首先,字典。

汉语是一种复杂的语言——难道它们不都是:-(。书写形式是象形文字,也就是“象形图”,而不是使用字母表。但这种书写形式随着时间的推移而演变,甚至最近分裂成两种形式:在台湾和香港使用的“繁体”中文,以及在 mainland China 使用的“简体”中文。虽然大多数字符是相同的,但大约有 1000 个字符是不同的。因此,一部汉语词典通常会有两种相同的书写形式。

像我这样的西方人,大多看不懂这些文字。所以有一种“拉丁化”的形式,叫做拼音,它是以拉丁字母为基础,用音标书写汉字。它不完全是拉丁字母,因为汉语是一种带声调的语言,拼音形式必须显示声调(很像法语和其他欧洲语言中的重音)。所以一个典型的字典必须显示四样东西:繁体、简体、拼音和英语。另外(就像英语里一样),一个词可能有多个意思。比如 http://www.mandarintools.com/worddict.html 有免费的中/英文词典,更好的是可以下载成 UTF-8 文件。在里面,这个词好有这个条目:

| 传统的 | 简化了的 | 拼音 | 英语 | 含义 | | --- | --- | --- | --- | --- | | good | good | 嘿!嘿 | 好的 | /good/well/proper/good to/easy to/very/so/(表示完成或准备就绪的后缀)/ |

这本字典有点复杂。大多数键盘不擅长表现重音,如中的卡隆音。因此,虽然汉字是用 Unicode 书写的,但拼音字符不是。虽然像\u 这样的字母有 Unicode 字符,但包括这本在内的许多词典都使用拉丁字母 a,并将音调放在单词的末尾。这里是第三声,所以 hǎo 写成 hao3。这使得那些只有美国键盘而没有 Unicode 编辑器的人仍然可以更容易地用拼音交流。网络服务器使用的字典的副本是cedict_ts_u8

这种数据格式不匹配不是什么大问题。只是在原始文本字典和浏览器显示之间的某个地方,必须执行数据消息。Go 模板允许通过定义一个自定义模板来实现这一点,所以我选择了这条路线。替代方法包括在读入字典时这样做,或者在 JavaScript 中显示最后的字符。

字典类型

我们使用一个Entry来保存一个单词的基本信息:

type Entry struct {
     Traditional string
     Simplified string
     Pinyin     string
     Translations []string
}

上面的单词将由以下内容表示:

Entry{Traditional: 好,
          Simplified: 好,
          Pinyin: `hao3`
          Translations: []string{`good`, `well`,`proper`,
                                           `good to`, `easy to`, `very`, `so`,  
                                           `(suffix indicating completion or readiness)`}
}

字典本身就是这些条目的数组:

type Dictionary struct {
      Entries []*Entry
}

抽认卡套装

一张抽认卡代表一个中文单词和这个单词的英文翻译。我们已经看到,一个单一的中文单词可以有许多可能的英文意思。但这部词典有时也会多次出现一个中文单词。举个例子,好至少出现两次,一次带有我们已经看到的意思,但也带有另一个意思,“喜欢”。这被证明是多余的,但是考虑到这一点,每个抽认卡都有一个完整的单词字典。通常字典中只有一个条目!抽认卡的其余部分只是作为可能的密钥的简化和英语单词:

type FlashCard struct {
        Simplified string
        English    string
        Dictionary *dictionary.Dictionary

}

抽认卡组是这些抽认卡的一个数组,加上抽认卡组的名称,以及将被发送到浏览器以显示抽认卡组的信息:随机或固定顺序,首先显示每张卡片的顶部或底部,或者随机。

type FlashCards struct {
        Name      string
        CardOrder string
        ShowHalf  string
        Cards     []*FlashCard
}

我们已经展示了这种类型的一个函数,ListFlashCardsNames()。这种类型还有一个有趣的功能,为抽认卡集加载 JSON 文件。这使用了第四章的技术,连载。

func LoadJSON(fileName string, key interface{}) {
        inFile, err := os.Open(fileName)
        checkError(err)
        decoder := json.NewDecoder(inFile)
        err = decoder.Decode(key)
        checkError(err)
        inFile.Close()
}

一套典型的抽认卡是由普通单词组成的。当 JSON 文件被 Python ( print json.dump(string, indent=4, separators=(',', ':')))漂亮地打印出来时,它的一部分看起来像这样:

{
    "ShowHalf":"",
    "Cards":[
        {
            "Simplified":"\u4f60\u597d",
            "Dictionary":{
                "Entries":[
                    {
                        "Traditional":"\u4f60\u597d",
                        "Pinyin":"ni3 hao3",
                        "Translations":[
                            "hello",

                            "hi",
                            "how are you?"

                        ],
                        "Simplified":"\u4f60\u597d"
                    }
                ]
            },
            "English":"hello"
        },

        {
            "Simplified":"\u5582",
            "Dictionary":{
                "Entries":[
                    {
                        "Traditional":"\u5582",
                        "Pinyin":"wei4",
                        "Translations":[
                            "hello (interj., esp. on telephone)",
                            "hey",
                            "to feed (sb or some animal)"
                        ],
                        "Simplified":"\u5582"
                    }
                ]
            },
            "English":"hello (interj., esp. on telephone)"
        },
    ],
    "CardOrder":"",
    "Name":"Common Words"

}

修正口音

在我们完成服务器的代码之前,还有最后一个主要任务。字典中给出的重音符号将重音符号放在拼音单词的末尾,如 hao3 中的 hǎo。如第九章所述,可以通过自定义模板将重音符号转换为 Unicode。

这里给出了拼音格式化程序的代码。除非你真的有兴趣了解拼音格式的规则,否则不要费心去读它。程序是PinyinFormatter.go:

package templatefuncs

import (
        "fmt"
        "strings"
)

func PinyinFormatter(args ...interface{}) string {
        ok := false
        var s string
        if len(args) == 1 {
                s, ok = args[0].(string)
        }
        if !ok {
                s = fmt.Sprint(args...)
        }
        fmt.Println("Formatting func " + s)
        // the string may consist of several pinyin words
        // each one needs to be changed separately and then
        // added back together
        words := strings.Fields(s)

        for n, word := range words {
                // convert "u:" to "ü" if present
                uColon := strings.Index(word, "u:")
                if uColon != -1 {
                        parts := strings.SplitN(word, "u:", 2)
                        word = parts[0] + "ü" + parts[1]
                }
                println(word)
                // get last character, will be the tone if present
                chars := []rune(word)
                tone := chars[len(chars)-1]

                if tone == '5' {
                        // there is no accent for tone 5
                        words[n] = string(chars[0 : len(chars)-1])
                        println("lost accent on", words[n])
                        continue
                }
                if tone < '1' || tone > '4' {
                        // not a tone value
                        continue
                }
                words[n] = addAccent(word, int(tone))
        }
        s = strings.Join(words, ` `)
        return s
}

var (
        // maps 'a1' to '\u0101' etc
        aAccent = map[int]rune{
                '1': '\u0101',
                '2': '\u00e1',
                '3': '\u01ce',
                '4': '\u00e0'}
        eAccent = map[int]rune{
                '1': '\u0113',
                '2': '\u00e9',
                '3': '\u011b',
                '4': '\u00e8'}
        iAccent = map[int]rune{
                '1': '\u012b',
                '2': '\u00ed',
                '3': '\u01d0',
                '4': '\u00ec'}
        oAccent = map[int]rune{
                '1': '\u014d',
                '2': '\u00f3',
                '3': '\u01d2',
                '4': '\u00f2'}
        uAccent = map[int]rune{
                '1': '\u016b',
                '2': '\u00fa',
                '3': '\u01d4',
                '4': '\u00f9'}
        üAccent = map[int]rune{

                '1': 'ǖ',
                '2': 'ǘ',
                '3': 'ǚ',
                '4': 'ǜ'}
)

func addAccent(word string, tone int) string {
        /*
         * Based on "Where do the tone marks go?"
         * at http://www.pinyin.info/rules/where.html
         */

        n := strings.Index(word, "a")
        if n != -1 {
                aAcc := aAccent[tone]
                // replace 'a' with its tone version
                word = word[0:n] + string(aAcc) + word[(n+1):len(word)-1]
        } else {
                n := strings.Index(word, "e")
                if n != -1 {
                        eAcc := eAccent[tone]
                        word = word[0:n] + string(eAcc) +
                                word[(n+1):len(word)-1]
                } else {
                        n = strings.Index(word, "ou")
                        if n != -1 {
                                oAcc := oAccent[tone]
                                word = word[0:n] + string(oAcc) + "u" +
                                        word[(n+2):len(word)-1]
                        } else {
                                chars := []rune(word)
                                length := len(chars)
                                // put tone on the last vowel
                        L:
                                for n, _ := range chars {
                                        m := length - n - 1
                                        switch chars[m] {
                                        case 'i':
                                                chars[m] = iAccent[tone]
                                                break L
                                        case 'o':
                                                chars[m] = oAccent[tone]
                                                break L
                                        case 'u':
                                                chars[m] = uAccent[tone]
                                                break L
                                        case 'ü':
                                                chars[m] = üAccent[tone]
                                                break L
                                        default:
                                        }

                                }
                                word = string(chars[0 : len(chars)-1])
                        }
                }
        }
        return word
}

ListWords 函数

我们现在可以回到服务器的突出功能。一个是在一套抽认卡中列出单词。这将使用抽认卡集的模板填充一个 HTML 表。HTML 使用模板包遍历一个FlashCards结构并插入该结构中的字段:

<html>
  <head>
    <title>
      Words for {{.Name}}
    </title>

  </head>
  <body>
    <h1>
      Words for {{.Name}}
    </h1>
    <p>
      <table border="1" class="sortable">
        <tr>
          <th> English </th>
          <th> Pinyin </th>
          <th> Traditional </th>
          <th> Simplified </th>
        </tr>
      {{range .Cards}}
        <div class="card">
          <tr>
          <div class="english">
            <div class="vcenter">
              <td>
                {{.English}}

              </td>
            </div>
          </div>

          {{with .Dictionary}}
            {{range .Entries}}
              <div class="pinyin">
                <div class="vcenter">
                  <td>
                    {{.Pinyin|pinyin}}
                  </td>
                </div>
              </div>

              <div class="traditional">
                <div class="vcenter">
                  <td>
                    {{.Traditional}}
                  </td>
                </div>
              </div>

              <div class="simplified">
                <div class="vcenter">
                  <td>
                    {{.Simplified}}
                  </td>
                </div>
              </div>

            {{end}}
          {{end}}
          </tr>
        </div>
      {{end}}
      </table>
    </p>
    <p class ="return">
      <a href="http:/flashcards.html"> Return to Flash Cards list</a>
    </p>
  </body>
</html>

为此,Server.go中的 Go 函数使用了上一节讨论的PinyinFormatter:

func listWords(rw http.ResponseWriter, cardname string) {
        cards := new(flashcards.FlashCards)
        flashcards.LoadJSON(cardname, cards)
        fmt.Println("Card name", cards.Name)

        t := template.New("ListWords.html")

        t = t.Funcs(template.FuncMap{"pinyin": templatefuncs.PinyinFormatter})
        t, err := t.ParseFiles("html/ListWords.html")

        if err != nil {
                fmt.Println("Parse error " + err.Error())
                http.Error(rw, err.Error(), http.StatusInternalServerError)
                return
        }
        err = t.Execute(rw, cards)
        if err != nil {
                fmt.Println("Execute error " + err.Error())
                http.Error(rw, err.Error(), http.StatusInternalServerError)
                return
        }
}

这会将填充的表格发送到浏览器,如图 10-4 所示。

showFlashCards 功能

完成服务器的最后一个函数是showFlashCards。这将根据浏览器提交的表单,改变抽认卡组中CardOrderShowHalf的默认值。然后应用PinyinFormatter并将结果文档发送给浏览器。我使用 UNIX 命令script捕获命令行会话的输出,然后运行命令:

GET /flashcardSets?flashcardSets=Common+Words&order=Random&half=Chinese&submit=Show+cards+in+set HTTP/1.0

部分结果如下:

<html>
  <head>
    <title>
      Flashcards for Common Words

    </title>

    <link type="text/css" rel="stylesheet"
          href="/html/CardStylesheet.css">
    </link>

    <script type="text/javascript"
            language="JavaScript1.2" src="/jscript/jquery.js">
      <!-- empty -->
    </script>

    <script type="text/javascript"
            language="JavaScript1.2" src="/jscript/slideviewer.js">
      <!-- empty -->
    </script>

    <script type="text/javascript"
            language="JavaScript1.2">
      cardOrder = "RANDOM";
      showHalfCard = "CHINESE_HALF";
    </script>
  </head>
  <body onload="showSlides();">

    <h1>
      Flashcards for Common Words
    </h1>
    <p>

        <div class="card">
          <div class="english">
            <div class="vcenter">
              hello
            </div>
          </div>

              <div class="pinyin">
                <div class="vcenter">
                  nǐ hǎo
                </div>
              </div>

              <div class="traditional">
                <div class="vcenter">
                  你好
                </div>
              </div>

              <div class="simplified">
                <div class="vcenter">
                  你好
                </div>
              </div>

              <div class ="translations">

                <div class="vcenter">

                  hello <br />

                  hi <br />

                  how are you? <br />

                </div>
              </div>

        </div>

浏览器上的演示

这个系统的最后一部分是如何在浏览器中显示这个 HTML。图 10-3 显示了一个由四部分组成的屏幕,显示英语、简体中文、备选翻译和繁体/简体对。这是如何通过下载到服务器的 JavaScript 程序完成的(这是使用FileServer Go 对象完成的)。JavaScript slideviewer.js文件实际上很长,因此在文本中被省略了。它包含在 http://www.apress.com/9781484226919 的程序文件中。

运行服务器

这是本书中第一个使用我们自己导入的文件的程序。所有以前的程序都只是使用了一个主文件和 Go 标准库。包的dictionary、抽认卡和pinyin中导入的文件需要组织好,以便go命令可以找到它们。

需要将环境变量GOPATH设置到一个目录中,该目录下有一个子目录src,该子目录包含适当子目录中导入的源文件:

src/flashcards/FlashCards.go
src/pinyin/PinyinFormatter.go
src/dictionary/Dictionary.go

然后,可以使用如下命令在端口8000(或其他端口)上运行服务器:

go run Server.go :8000

结论

本章考虑了一个相对简单但完整的 web 服务器,它使用静态和动态 web 页面处理表单,并使用模板来简化编码。

十一、HTML

Web 最初是为 HTML 文档服务而创建的。现在它被用来服务各种各样的文件和不同种类的数据。然而,HTML 仍然是通过网络传递的主要文档类型。

HTML 经历了大量的版本,目前的版本是 HTML5。也有许多 HTML 的“供应商”版本,引入了从未成为标准的标签。

HTML 非常简单,可以手工编辑。因此,许多 HTML 文档是“格式错误的”,这意味着它们不遵循语言的语法。HTML 解析器通常不是很严格,会接受许多“非法”文档。

HTML 包本身只有两个功能——EscapeStringUnescapeString。这些适当地处理角色,例如<,将它们转换成&lt;,然后再转换回来。

这种方法的主要用途可能是对 HTML 文档中的标记进行转义,这样如果在浏览器中显示,就会显示所有的标记(很像 Linux 上 Chrome 中的 Ctrl+U 或 Mac Chrome 上的 Option+Cmd+U)。

我更倾向于用这个把程序的文本显示成网页。大多数编程语言都有<符号,许多有&。除非正确转义,否则这些会搞乱 HTML 查看器。我喜欢直接从文件系统中显示程序文本,而不是复制粘贴到文档中,以避免不同步。

下面的程序EscapeString.go是一个 web 服务器,它以预格式化的代码显示其 URL,并对麻烦的字符进行了转义:

/*
 * This program serves a file in preformatted, code layout
 * Useful for showing program text, properly escaping special
 * characters like '<', '>' and '&'
 */

package main

import (
        "fmt"
        "html"
        "io/ioutil"
        "net/http"
        "os"
)

func main() {
        http.HandleFunc("/", escapeString)

        err := http.ListenAndServe(":8080", nil)
        checkError(err)

}

func escapeString(rw http.ResponseWriter, req *http.Request) {
        fmt.Println(req.URL.Path)
        bytes, err := ioutil.ReadFile("." + req.URL.Path)
        if err != nil {
                rw.WriteHeader(http.StatusNotFound)
                return
        }

        escapedStr := html.EscapeString(string(bytes))
        htmlText := "<html><body><pre><code>" +
                escapedStr +
                " </code></pre></body></html>"
        rw.Write([]byte(htmlText))
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Error ", err.Error())
                os.Exit(1)
        }
}

当它运行时,从包括EscapeString.go程序的目录中提供文件,浏览器将使用 URL localhost:8080/EscapeString.go正确显示它。

使用以下命令运行服务器:

go run EscapeString.go

例如,使用以下命令运行客户端:

curl localhost:8080/EscapeString.go

Go HTML/模板包

对 web 服务器的攻击有很多种,其中最著名的是 SQL 注入,用户代理将数据输入到 web 表单中,该表单被故意设计为传递到数据库并在那里造成严重破坏。Go 没有任何特定的支持来避免这种情况,因为对于可以成功的 SQL 注入技术,数据库之间存在许多差异。SQL 注入预防备忘单(参见 https://www.owasp.org/index.php/SQL_Injection_Prevention_Cheat_Sheet )总结了针对此类攻击的防御措施。主要的一点是通过使用 SQL 预准备语句来避免这种攻击,这可以通过使用database/sql包中的Prepare函数来完成。

更微妙的攻击是基于 XSS——跨站点脚本。在这种情况下,攻击者不是试图攻击网站本身,而是在服务器上存储恶意代码,以攻击该网站的任何客户端。

这些攻击基于将数据插入到数据库字符串中,例如,当将数据传送到浏览器时,将攻击浏览器,并通过它攻击网站的客户端。(这有几种变体,在“OWASP:跨站点脚本的类型”——https://www.owasp.org/index.php/Types_of_Cross-Site_Scripting中讨论过)。)

例如,可以在请求博客评论的地方插入 JavaScript,以将浏览器重定向到攻击者的站点:

<script>
   window.location='http://attacker/'
</script>

Go html/ template包设计在text/template包之上。假设模板是可信的,但它处理的数据可能不可信。html/template增加的是数据的适当转义,以尽量消除 XSS 的可能性。它基于由 Mike Samuel 和 Prateek Saxena 撰写的名为“使用类型推断使 Web 模板抵抗 XSS”的文档。请在 https://rawgit.com/mikesamuel/sanitized-jquery-templates/trunk/safetemplate.html#problem_definition 阅读该论文,了解软件包背后理论以及软件包文档本身。

简而言之,按照text/template包准备模板,如果结果文本被交付给 HTML 代理,则使用html/template包。

标记 HTML

Go 子仓库中的包golang.org/x/net/html包含一个 HTML 标记器。这允许您构建 HTML 标记的解析树。它符合 HTML5。

运行以下命令后可以使用它:

go get golang.org/x/net/html

使用它的一个示例程序是ReadHTML.go:

/* Read HTML
 */

package main

import (
        "fmt"
        "golang.org/x/net/html"

        "io/ioutil"
        "os"
        "strings"
)

func main() {
        if len(os.Args) != 2 {
                fmt.Println("Usage: ", os.Args[0], "file")
                os.Exit(1)
        }
        file := os.Args[1]
        bytes, err := ioutil.ReadFile(file)
        checkError(err)
        r := strings.NewReader(string(bytes))

        z := html.NewTokenizer(r)

        depth := 0
        for {
                tt := z.Next()

                for n := 0; n < depth; n++ {
                        fmt.Print(" ")
                }

                switch tt {
                case html.ErrorToken:
                        fmt.Println("Error ", z.Err().Error())
                        os.Exit(0)
                case html.TextToken:
                        fmt.Println("Text: \"" + z.Token().String() + "\"")
                case html.StartTagToken, html.EndTagToken:
                        fmt.Println("Tag: \"" + z.Token().String() + "\"")
                        if tt == html.StartTagToken {
                                depth++
                        } else {
                                depth--
                        }

                }
        }

}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

当它在一个简单的 HTML 文档上运行时,如下所示:

<html>
  <head>
    <title> Test HTML </title>
  </head>
  <body>
    <h1> Header one </h1>
    <p>
      Test para
    </p>
  </body>
</html>

它产生以下内容:

Tag: "<html>"
 Text: "
  "
 Tag: "<head>"
  Text: "
    "
  Tag: "<title>"
   Text: " Test HTML "
   Tag: "</title>"
  Text: "
  "
  Tag: "</head>"
 Text: "
  "
 Tag: "<body>"
  Text: "
    "
  Tag: "<h1>"
   Text: " Header one "
   Tag: "</h1>"
  Text: "
    "
  Tag: "<p>"
   Text: "
      Test para
    "
   Tag: "</p>"
  Text: "
  "
  Tag: "</body>"

 Text: "
"
 Tag: "</html>"
Text: "
"

(它产生的所有空白都是正确的。)

XHTML/HTML

XML 包中对 XHTML/HTML 的支持也是有限的,这将在下一章讨论。

数据

JSON 有很好的支持,如第四章所讨论的。

结论

这个包裹没什么特别的。关于模板的第九章讨论了子包html/template

十二、XML

XML 是一种重要的标记语言,主要用于以文本格式表示结构化数据。在我们在第四章使用的语言中,它可以被认为是将数据结构序列化为文本文档的一种手段。它用于描述 DocBook 和 XHTML 等文档。它用于专门的标记语言,如 MathML 和 CML(化学标记语言)。它用于将数据编码为 Web 服务的 SOAP 消息,并且可以使用 WSDL (Web 服务描述语言)来指定 Web 服务。

在最简单的层面上,XML 允许您定义自己的标签,以便在文本文档中使用。标签可以嵌套,也可以穿插文本。每个标签还可以包含带值的属性。例如,文件person.xml可能包含:

<person>
  <name>
    <family> Newmarch </family>
    <personal> Jan </personal>
  </name>
  <email type="personal">
    jan@newmarch.name
  </email>
  <email type="work">
    j.newmarch@boxhill.edu.au
  </email>
</person>

任何 XML 文档的结构都可以用多种方式描述:

  • 文档类型定义 DTD 有利于描述结构
  • XML 模式适合描述 XML 文档使用的数据类型
  • RELAX NG 被提议作为两者的替代方案

对于定义 XML 文档结构的每种方法的相对价值存在争议。我们不会买那个,因为 Go 不支持任何一个。Go 不能根据模式检查任何文档的有效性,只能检查文档的格式是否良好。甚至良构性也是 XML 文档的一个重要特征,并且在实践中经常是 HTML 文档的一个问题。这使得 XML 适合于表示非常复杂的数据,而 HTML 不适合。

本章讨论了四个主题:解析 XML 流、将 Go 数据编组和解组成 XML 以及 XHTML。

解析 XML

Go 有一个 XML 解析器,它是使用来自encoding/xml包的NewDecoder创建的。这将一个io.Reader作为参数,并返回一个指向Decoder的指针。这种类型的主要方法是Token,它返回输入流中的下一个令牌。令牌是这些类型之一— StartElementEndElementCharDataCommentProcInstDirective

我们将使用这种类型:

type Name struct {
    Space, Local string
}

XML 类型有StartElementEndElementCharDataCommentProcInstDirective。接下来将对它们进行描述。

startellemon 类型

类型StartElement是具有两种字段类型的结构:

type StartElement struct {
    Name Name
    Attr []Attr
}

在哪里

type Attr struct {
    Name  Name
    Value string
}

EndElement 类型

这也是如下的结构:

type EndElement struct {
    Name Name
}

CharData 类型

这种类型表示由标记括起的文本内容,是一种简单类型:

type CharData []byte

注释类型

类似地,对于这种类型:

type Comment []byte

ProcInst 类型

A ProcInst表示形式为<?target inst?>的 XML 处理指令:

type ProcInst struct {
    Target string
    Inst   []byte
}

指令类型

一个Directive表示一个形式为<!text>的 XML 指令。这些字节不包括<!>标记。

type Directive []byte

打印出 XML 文档树形结构的程序是ParseXML.go:

/* Parse XML
 */

package main

import (
        "encoding/xml"
        "fmt"
        "io/ioutil"
        "os"
        "strings"
)

func main() {
        if len(os.Args) != 2 {
                fmt.Println("Usage: ", os.Args[0], "file")
                os.Exit(1)
        }
        file := os.Args[1]
        bytes, err := ioutil.ReadFile(file)
        checkError(err)
        r := strings.NewReader(string(bytes))

        parser := xml.NewDecoder(r)
        depth := 0
        for {
                token, err := parser.Token()
                if err != nil {
                        break

                }
                switch t := token.(type) {
                case xml.StartElement:
                        elmt := xml.StartElement(t)
                        name := elmt.Name.Local
                        printElmt(name, depth)
                        depth++
                case xml.EndElement:
                        depth--
                        elmt := xml.EndElement(t)
                        name := elmt.Name.Local
                        printElmt(name, depth)
                case xml.CharData:
                        bytes := xml.CharData(t)
                        printElmt("\""+string([]byte(bytes))+"\"", depth)
                case xml.Comment:
                        printElmt("Comment", depth)
                case xml.ProcInst:
                        printElmt("ProcInst", depth)
                case xml.Directive:
                        printElmt("Directive", depth)
                default:
                        fmt.Println("Unknown")
                }
        }
}

func printElmt(s string, depth int) {
        for n := 0; n < depth; n++ {
                fmt.Print("  ")
        }
        fmt.Println(s)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

注意,解析器包含所有的CharData,包括标签之间的空白。

如果我们针对前面给出的person数据结构运行这个程序,如下所示:

go run ParseXML.go person.xml

它产生以下内容:

person
  "
  "
  name
    "
    "
    family
      " Newmarch "
    family
    "
    "
    personal
      " Jan "
    personal
    "
  "
  name
  "
  "
  email
    "
    jan@newmarch.name
  "
  email
  "
  "
  email
    "
    j.newmarch@boxhill.edu.au
  "
  email
  "
"
person
"
"

注意,因为没有使用 DTD 或其他 XML 规范,所以标记器正确地打印出了所有的空白(DTD 可能指定空白可以忽略,但是没有它就不能做出这样的假设)。

使用这个解析器有一个潜在的陷阱。它为字符串重用空间,因此一旦看到一个令牌,如果以后想引用它,就需要复制它的值。Go 有像func (c CharData) Copy() CharData这样的方法来制作数据的副本。

解组 XML

Go 提供了一个名为Unmarshal的函数来将 XML 解组到 Go 数据结构中。解组并不完美:Go 和 XML 是不同的语言。

在看细节之前,我们先考虑一个简单的例子。首先考虑前面给出的 XML 文档:

<person>
  <name>
    <family> Newmarch </family>
    <personal> Jan </personal>
  </name>
  <email type="personal">
    jan@newmarch.name
  </email>
  <email type="work">
    j.newmarch@boxhill.edu.au
  </email>
</person>

我们希望将其映射到 Go 结构中:

type Person struct {
        Name Name
        Email []Email
}

type Name struct {
        Family string
        Personal string
}

type Email struct {
        Type string
        Address string
}

这需要几点说明:

  • 解组使用 Go 反射包。这要求所有字段都是公共的,即以大写字母开头。早期版本的 Go 使用不区分大小写的匹配来匹配字段,比如 XML 字符串“name”和字段Name。不过,现在使用的是区分大小写的匹配。要执行匹配,必须标记结构字段,以显示将要匹配的 XML 字符串。这将Person更改为以下内容:

    type Person struct {
            Name Name `xml:"name"`
            Email []Email `xml:"email"`
    }
    
    
  • 虽然对字段进行标记可以将 XML 字符串附加到字段上,但它不能对结构名进行标记。需要一个附加字段,字段名为XMLName。这只影响顶级结构,Person :

    type Person struct {
            XMLName Name `xml:"person"`
            Name Name `xml:"name"`
            Email []Email `xml:"email"`
    }
    
    
  • 重复的标签映射到 Go 中的一个切片。

  • 只有当 Go 字段具有标签,attr时,标签中的属性才会与结构中的字段匹配。这发生在Email的字段Type中,其中匹配email标签的属性type需要xml:"type,attr"

  • 如果一个 XML 标签没有属性,只有字符数据,那么它匹配一个同名的string字段(尽管区分大小写)。因此带有字符数据Newmarch的标签xml:"family"映射到字符串字段Family

  • 但是如果标签有属性,那么它必须映射到一个结构。Go 将字符数据分配给标签为,chardata的字段。这发生在email数据和tag ,chardata字段Address中。

解组上面文档的程序是Unmarshal.go:

/* Unmarshal
 */

package main

import (
        "encoding/xml"
        "fmt"
        "os"
)

type Person struct {
        XMLName Name    `xml:"person"`
        Name    Name    `xml:"name"`
        Email   []Email `xml:"email"`
}

type Name struct {
        Family   string `xml:"family"`
        Personal string `xml:"personal"`
}

type Email struct {
        Type    string `xml:"type,attr"`
        Address string `xml:",chardata"`
}

func main() {
        str := `<?xml version="1.0" encoding="utf-8"?>
<person>
  <name>
    <family> Newmarch </family>
    <personal> Jan </personal>
  </name>
  <email type="personal">
    jan@newmarch.name
  </email>
  <email type="work">
    j.newmarch@boxhill.edu.au
  </email>
</person>`

        var person Person

        err := xml.Unmarshal([]byte(str), &person)
        checkError(err)

        // now use the person structure e.g.
        fmt.Println("Family name: \"" + person.Name.Family + "\"")
        fmt.Println("Second email address: \"" + person.Email[1].Address + "\"")
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

(注意空格是正确的。)封装规范中给出了严格的规则。

编组 XML

Go 还支持将数据结构组织成 XML 文档。功能是:

func Marshal(v interface}{) ([]byte, error)

整理一个简单结构的程序是Marshal.go:

/* Marshal
 */

package main

import (
        "encoding/xml"
        "fmt"
)

type Person struct {
        Name  Name
        Email []Email
}

type Name struct {
        Family   string
        Personal string
}

type Email struct {
        Kind    string "attr"
        Address string "chardata"
}

func main() {
        person := Person{
                Name: Name{Family: "Newmarch", Personal: "Jan"},
                Email: []Email{Email{Kind: "home", Address: "jan"},
                        Email{Kind: "work", Address: "jan"}}}

        buff, _ := xml.Marshal(person)
        fmt.Println(string(buff))
}

它生成不带空格的文本:

<Person><Name><Family>Newmarch</Family><Personal>Jan</Personal></Name><Email><Kind>home</Kind><Address>jan</Address></Email><Email><Kind>work</Kind><Address>jan</Address></Email></Person>

可扩展的超文本标记语言

HTML 不符合 XML 语法。它有未终止的标签,如<br>。XHTML 是对 HTML 的清理,使其符合 XML。XHTML 中的文档可以使用上述 XML 技术进行管理。XHTML 似乎没有像最初预期的那样被广泛使用。我个人的怀疑是,HTML 解析器通常是容忍错误的,当在浏览器中使用时,通常可以合理地呈现文档,即使在浏览器中,XHTML 解析器也往往更加严格,经常在遇到甚至一个 XML 错误时也不能呈现任何内容。对于面向用户的软件来说,这通常不是合适的行为。

超文本标记语言

XML 包中有一些处理 HTML 文档的支持,即使它们可能不符合 XML。如果关闭严格的解析检查,前面讨论的 XML 解析器可以处理许多 HTML 文档。

parser := xml.NewDecoder(r)
parser.Strict = false
parser.AutoClose = xml.HTMLAutoClose
parser.Entity = xml.HTMLEntity

结论

Go 具有处理 XML 字符串的基本支持。它还没有处理 XML 规范语言(如 XML Schema 或 Relax NG)的机制。