前面提到了Web应用和Web服务的区别,Web服务就是一个向其他软件程序提供服务的程序。本章将扩展这一定义,并展示如何使用Go语言来编写或使用Web服务。
1. 基于REST的Web服务
REST(Representational State Transfer,具象状态传输)是一种设计理念,用于设计那些通过标准的几个动作来操纵资源,并以此来进行相互交流的程序。
在OOP面向对象编程中,人们通过创建称为对象(object)的模型来表示事物,然后定义称为方法(method)的函数并将它们附着到模型之上。REST是以上思想的进化版,但它并不是把函数暴露(expose)为可调用的服务,而是以资源(resource)的名义把模型暴露出来,并允许人们通过少数几个称为动词的动作来操纵这些资源。
在使用HTTP协议实现REST服务时,URL将用于表示资源,而HTTP方法则会用作操纵资源的动词:
2. XML
下面介绍go语言是如何实现REST服务的。
2.1 分析XML
XML可以以结构化的形式表示数据,跟HTML一样,都是一种流行的标记语言。
在Go语言里面,用户首先需要将XML的分析结果存储到一些结构里面,然后通过访问这些结构来获取XML记录的数据。
- 创建一些用于存储XML数据的结构;
- 使用
xml.Unmarshal
将XML数据解封(unmarshal)到结构里面
下面举个例子:
首先创建一个XML文件
<?xml version="1.0" encoding="utf-8"?>
<post id="1">
<content>Hello World!</content>
<author id="2">Sau Sheong</author>
</post>
在这个XML文件中,包含了一个post结构,由id
决定,成员包括了纯粹文本的content
和以结构形式出现的author
,而author
由id
决定。
--- post
--- id
--- content
--- author
--- id
--- author
由此我们在程序中定义结构,用于表示数据:
type Post struct{
XMLname xml.Name `xml:"post"`
Id string `xml:"id,attr"`
Content string `xml:"content"`
Author Author `xml:"author"`
Xml string `xml:",innerxml"`
}
type Author struct {
Id string `xml:"id,attr"`
Name string `xml:",chardata"`
}
Post
结构中每个字段的定义后面都带有一段使用反引号(`)包围的信息,这些信息被称为结构标签。
出于创建映射的需要,xml包要求被映射的结构以及结构包含的所有字段都必须是公开的,也就是,它们的名字必须以大写的英文字母开头。以上面展示的代码为例,结构的名字必须为Post而不能是post,至于字段的名字则必须为Content而不能是content。
XML使用的详细规则如下:
- 通过创建一个名字为
XMLName
、类型为xml.Name
的字段,可以将XML元素的名字存储在这个字段里面(在一般情况下,结构的名字就是元素的名字)。 - 有模式标志
- 通过创建一个与XML元素属性同名的字段,并使用
'xml:"<name>,attr"'
作为该字段的结构标签,可以将元素的<name>
属性的值存储到这个字段里面。 - 通过创建一个与XML元素标签同名的字段,并使用
'xml:",chardata"
‘作为该字段的结构标签,可以将XML元素的字符数据存储到这个字段里面。 - 通过定义一个任意名字的字段,并使用
'xml:",innerxml"
‘作为该字段的结构标签,可以将XML元素中的原始XML存储到这个字段里面。
- 通过创建一个与XML元素属性同名的字段,并使用
- 没有模式标志的结构字段将与同名的
XML
元素匹配。 - 使用
'xml:"a>b>c
“‘这样的结构标签可以在不指定树状结构的情况下直接获取指定的XML元素,其中a和b为中间元素,而c则是想要获取的节点元素。
我们按照规则,对结构逐一分析:
根据规则1:分析程序将XML文件中的元素名字post
存储到了Post
结构体的XMLName
字段里面。
根据规则2a:分析程序通过结构标签xml:"id,attr
“将XML文件中的id
属性的值存储到了Post
结构的Id
字段里面。
根据规则3:分析程序通过结构标签'xml:"content"'
将content
子元素包含的字符数据存储到了Post
结构的Content
字段里面。
根据规则2c:分析程序定义了一个Xml字段,并使用'xml:",in-nerxml"'
作为该字段的结构标签,以此来获得被post
元素包含的原始XML:
<content>Hello World!</content>
<author id="2">Sau Sheong</author>
根据规则3,子元素author
拥有id
属性,并且包含字符数据SauSheong
,为了正确地构建映射,分析程序专门定义了Author
结构:
type Author struct {
Id string `xml:"id,attr"`
Name string `xml:",chardata"`
}
运行后输出:
{{ post} 1 Hello World! {2 Sau Sheong}
<content>Hello World!</content>
<author id="2">Sau Sheong</author>
}
下面展示规则4的使用:
< ?xml version="1.0" encoding="utf-8"?>
< post id="1">
< content>Hello World!< /content>
< author id="2">Sau Sheong< /author>
<!-- 新添加的代码部分 start-->
< comments>
< comment id="1">
< content>Have a great day!< /content>
< author id="3">Adam< /author>
< /comment>
< comment id="2">
< content>How are you today?< /content>
< author id="4">Betty< /author>
< /comment>
< /comments>
<!-- 新添加的代码部分 end-->
< /post>
新添加代码定义了一个名为comments
的XML子元素,并且这个元素本身也包含多个comment
子元素。正常来说分析程序需要获取帖子的评论列表,但为此专门创建一个Comments
结构可能会显得有些小题大做了。为了简化实现代码,分析程序将根据规则56对comments
这个XML子元素进行跳跃式访问。
首先修改Post结构:
type Post struct {
XMLName xml.Name `xml:"post"`
Id string `xml:"id,attr"`
Content string `xml:"content"`
Author Author `xml:"author"`
Xml string `xml:",innerxml"`
Comments []Comment `xml:"comments>comment"`
}
通过结构标签'xml:"comments>comment"'
将这个字段映射至名为comment
的XML子元素。根据规则5,这一结构标签将允许分析程序跳过XML中的comments
元素,直接访问comment
子元素。
Comment
结构和Post
结构非常相似,它的具体定义如下:
type Comment struct {
Id string `xml:"id,attr"`
Content string `xml:"content"`
Author Author `xml:"author"`
}
这种做法虽然能够很好地处理体积较小的XML文件,但是却无法高效地处理以流(stream)方式传输的XML文件以及体积较大的XML文件。为了解决这个问题,我们需要使用Decoder
结构来代替Unmarshal
函数,通过手动解码XML元素的方式来解封XML数据,这个过程如图所示。
示例如下:
func main() {
xmlFile, err := os.Open("post.xml")
if err != nil {
fmt.Println("Error opening XML file:", err)
return
}
defer xmlFile.Close()
decoder := xml.NewDecoder(xmlFile)//①根据给定的XML数据生成相应的解码器
for {//②每迭代一次解码器中的所有XML数据
t, err := decoder.Token()//③每进行一次迭代,就从解码器里面获取一个token
if err == io.EOF {
break
}
if err != nil {
fmt.Println("Error decoding XML into tokens:", err)
return
}
switch se := t.(type) {//④检查token的类型
case xml.StartElement:
if se.Name.Local == "comment" {
var comment Comment
decoder.DecodeElement(&comment, &se)//⑤将XML数据解码至结构
}
}
}
}
虽然这段代码只演示了如何解码comment
元素,但这种解码方式同样可以应用于XML文件中的其他元素。这个新的分析程序会通过Decoder
结构,一个元素接一个元素地对XML进行解码,而不是像之前那样,使用Unmarshal
函数一次将整个XML解封为字符串。
2.2 创建XML
go中将结构封装为XML用到了函数marshal
。
装程序首先需要创建表示帖子的post
结构,并向结构里面填充数据,然后只要调用Marshal
函数,就可以根据Post
结构创建相应的XML了
func main() {
post := Post{
Id: "1",
Content: " Hello World!",//①创建结构并向里面填充数据
Author: Author{
Id: "2",
Name: "Sau Sheong",
},
}
output, err := xml.Marshal(&post)
_ = ioutil.WriteFile("post.xml", output, 0644)
}
这样输出的XML是没有换行的,格式不太好,可以使用MarshalIndent
函数。
output, err := xml.MarshalIndent(&post, "", "\t")
MarshalIndent
函数还接受两个额外的参数,这两个参数分别用于指定添加到每个输出行前面的前缀以及缩进,其中缩进的数量会随着元素的嵌套层次增加而增加。
但是这段输出还没有添加XML声明,我们需要手动添加:
err = ioutil.WriteFile("post.xml", []byte(xml.Header + string(output)), 0644)
同理,也可以手动使用Encoder
编码:
encoder := xml.NewEncoder(xmlFile)// 根据给定的XML文件,创建出相应的编码器
encoder.Indent("", "\t")
err = encoder.Encode(&post)// 把结构编码至文件
3. JSON
3.1 分析JSON
JSON(JavaScript Object Notation)是衍生自JavaScript语言的一种轻量级的文本数据格式,这种格式的主要设计理念是既能够轻易地被人类读懂,又能够简单地被机器读取。
JSON分析的过程和XML非常相似:
- 创建一些用于包含JSON数据的结构;
- 通过
json.Unmarshal
函数,把JSON数据解封到结构里面。
跟映射XML相比,把结构映射至JSON要简单得多,后者只有一条通用的规则:对于名字为<name>
的JSON键,用户只需要在结构里创建一个任意名字的字段,并将该字段的结构标签设置为'json:"<name>"'
,就可以把JSON键<name>
的值存储到这个字段里面。
JSON文件:
{
"id" : 1,
"content" : "Hello World!",
"author" : {
"id" : 2,
"name" : "Sau Sheong"
},
"comments" : [
{
"id" : 3,
"content" : "Have a great day!",
"author" : "Adam"
},
{
"id" : 4,
"content" : "How are you today?",
"author" : "Betty"
}
]
}
go的json分析程序:
type Post struct {
Id int `json:"id"`// 定义一些结构,用于表示数据
Content string `json:"content"`
Author Author `json:"author"`
Comments []Comment `json:"comments"`
}
type Author struct {
Id int `json:"id"`
Name string `json:"name"`
}
type Comment struct {
Id int `json:"id"`
Content string `json:"content"`
Author string `json:"author"`
}
func main() {
jsonFile, err := os.Open("post.json")
defer jsonFile.Close()
jsonData, err := ioutil.ReadAll(jsonFile)
var post Post
json.Unmarshal(jsonData, &post)//将JSON数据解封至结构
fmt.Println(post)
}
为了将JSON键id
的值映射到Post
结构的Id
字段,程序将该字段的结构标签设置成了'json:"id"'
,这种设置基本上就是将结构映射至JSON数据所需完成的全部工作。
当然也可以用Decoder
手动地将JSON数据解码到结构里面。
jsonFile, err := os.Open("post.json")
defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)//根据给定的JSON文件,创建出相应的解码器
for {//遍历JSON文件,直到遇见EOF为止
var post Post
err := decoder.Decode(&post)//将JSON数据解码至结构
if err == io.EOF {
break
}
fmt.Println(post)
}
3.2 创建JSON
创建过程和XML类似,首先创建结构,然后调用函数将其封装为JSON数据。
type Post struct {//创建结构并向里面填充数据
Id int `json:"id"`
Content string `json:"content"`
Author Author `json:"author"`
Comments []Comment `json:"comments"`
}
type Author struct {
Id int `json:"id"`
Name string `json:"name"`
}
type Comment struct {
Id int `json:"id"`
Content string `json:"content"`
Author string `json:"author"`
}
func main() {
post := Post{
Id: 1,
Content: "Hello World!",
Author: Author{
Id: 2,
Name: "Sau Sheong",
},
Comments: []Comment{
Comment{
Id: 3,
Content: "Have a great day!",
Author: "Adam",
},
Comment{
Id: 4,
Content: "How are you today?",
Author: "Betty",
},
},
}
output, err := json.MarshalIndent(&post, "", "\t\t")//把结构封装为由字节切片组成的JSON数据
err = ioutil.WriteFile("post.json", output, 0644)
}