前面提到了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记录的数据

  1. 创建一些用于存储XML数据的结构;
  2. 使用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,而authorid决定。

--- 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使用的详细规则如下:

  1. 通过创建一个名字为XMLName、类型为xml.Name的字段,可以将XML元素的名字存储在这个字段里面(在一般情况下,结构的名字就是元素的名字)。
  2. 有模式标志
    1. 通过创建一个与XML元素属性同名的字段,并使用'xml:"<name>,attr"'作为该字段的结构标签,可以将元素的<name>属性的值存储到这个字段里面。
    2. 通过创建一个与XML元素标签同名的字段,并使用'xml:",chardata"‘作为该字段的结构标签,可以将XML元素的字符数据存储到这个字段里面。
    3. 通过定义一个任意名字的字段,并使用'xml:",innerxml"‘作为该字段的结构标签,可以将XML元素中的原始XML存储到这个字段里面。
  3. 没有模式标志的结构字段将与同名的XML元素匹配。
  4. 使用'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非常相似:

  1. 创建一些用于包含JSON数据的结构;
  2. 通过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)  
}