golang web学习随便记7-XML、JSON、Web服务

XML解析与生成

Unmarshal方式解析

xml的解析比json复杂,最起码xml在元素的属性中也能蕴藏信息。下面是一个xml文件post1.xml:

<?xml version="1.0" encoding="utf-8" ?>
<post id="1">
    <content>你好,Golang</content>
    <author id="2">张三</author>
</post>

表面上看,上述xml就是外层元素post里面有两个子元素content和author,但post和author都是带有id属性的,其实这意味着author有2项信息,不能用一个字段表达,所以,下面的代码为author定义了结构体。

package main

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

type Post struct {
	XMLName xml.Name `xml:"post"`      // 元素名
	Id      string   `xml:"id,attr"`   // 元素的属性,模式标志 attr
	Content string   `xml:"content"`   // “常规”子元素
	Author  Author   `xml:"author"`    // 复合型子元素
	Xml     string   `xml:",innerxml"` // 元素内部的原始xml,模式标志 innerxml
}

type Author struct {
	Id   string `xml:"id,attr"`
	Name string `xml:",chardata"` // 元素的字符数据,模式标志 chardata
}

func main() {
	xmlFile, err := os.Open("post1.xml")
	if err != nil {
		fmt.Println("打开xml文件错误:", err)
		return
	}
	defer xmlFile.Close()
	xmlData, err := ioutil.ReadAll(xmlFile)
	if err != nil {
		fmt.Println("读取xml数据错误:", err)
		return
	}
	var post Post
	xml.Unmarshal(xmlData, &post)
	fmt.Println(post)
}

运行后输出结构为:

sjg@sjg-PC:~/go/src/xml$ go run .
{{ post} 1 你好,Golang {2 张三} 
    <content>你好,Golang</content>
    <author id="2">张三</author>
}

我们修改一下post1.xml为以下post2.xml,设想返回该帖子时同时返回了对应的2条评论:

<?xml version="1.0" encoding="utf-8" ?>
<post id="1">
    <content>你好,Golang</content>
    <author id="2">张三</author>
    <comments>
        <comment id="1">
            <content>C++才是好语言</content>
            <author id="3">李四</author>
        </comment>
        <comment id="2">
            <content>Rust比C++好</content>
            <author id="4">王五</author>
        </comment>        
    </comments>
</post>

在修改解析代码前,我们先需要考虑:是否需要为comments元素定义一个结构体,然后再为comment元素定义一个结构体,前者是后者的列表?更广泛的,我们是否必须定义上级元素才能获取到下一级元素?答案是否定的。我们可以使用`xml:"a>b>c"`这样的结构标签实现不指定树状结构的情况下直接获取指定的XML元素(即跳过中间元素a和b,直接获取c)。代码添加或修改如下:

// ....................................................
type Post struct {
    // ...................................
	Xml      string    `xml:",innerxml"` // 元素内部的原始xml,模式标志 innerxml
	Comments []Comment `xml:"comments>comment"`
}

type Author struct {
    // ...................................
}

type Comment struct {
	Id      string `xml:"id,attr"`
	Content string `xml:"content"`
	Author  Author `xml:"author"`
}

func main() {
	xmlFile, err := os.Open("post2.xml")
	// ......................................
}

注意上面对列表的处理。结果输出为:

sjg@sjg-PC:~/go/src/xml$ go run .
{{ post} 1 你好,Golang {2 张三} 
    <content>你好,Golang</content>
    <author id="2">张三</author>
    <comments>
        <comment id="1">
            <content>C++才是好语言</content>
            <author id="3">李四</author>
        </comment>
        <comment id="2">
            <content>Rust比C++好</content>
            <author id="4">王五</author>
        </comment>        
    </comments>
 [{1 C++才是好语言 {3 李四}} {2 Rust比C++好 {4 王五}}]}

Decoder方式解析

unmarshal这种方式可以一次性获取所有信息,但这种方式适合体积不太大的xml。对于大个头xml,我们需要使用 Decoder 来手动解码:

//  ..........................
	defer xmlFile.Close()

	decoder := xml.NewDecoder(xmlFile)
	for {
		t, err := decoder.Token()
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("解码 xml 到令牌出错:", err)
			return
		}
		switch se := t.(type) {        // 判断令牌类型
		case xml.StartElement:        // 是xml某个元素起始标签
			if se.Name.Local == "comment" {  // 元素名为 comment
				var comment Comment
				decoder.DecodeElement(&comment, &se)
				fmt.Println(comment)
			}
		}
	}
}
sjg@sjg-PC:~/go/src/xml$ go run .
{1 C++才是好语言 {3 李四}}
{2 Rust比C++好 {4 王五}}

Marshal方式生成

要生成xml比解析要容易一些,并且也有2种方式:marshal方式或者encoder方式

// .............结构体与前面的代码相同....................

func main() {
	post := Post{
		Id:      "1",
		Content: "你好,Golang",
		Author: Author{
			Id:   "2",
			Name: "张三",
		},
		Comments: []Comment{
			{
				Id:      "1",
				Content: "C++才是好语言",
				Author: Author{
					Id:   "3",
					Name: "李四",
				},
			},
			{
				Id:      "2",
				Content: "Rust比C++好",
				Author: Author{
					Id:   "4",
					Name: "王五",
				},
			},
		},
	}
	output, err := xml.MarshalIndent(&post, "", "    ") // 用 Marshal 则不缩进
	if err != nil {
		fmt.Println("列集到xml出错:", err)
		return
	}
	err = ioutil.WriteFile("post.xml", []byte(xml.Header+string(output)), 0644)
	if err != nil {
		fmt.Println("写入xml文件出错:", err)
		return
	}
}

生成的文件带了缩进,这里不再列出。

encoder方式生成

下面改写的代码,是用编码器方式生成xml文件:

	post := Post{
//  ..........................................
    }
	xmlFile, err := os.Create("post2.xml")
	if err != nil {
		fmt.Println("创建xml文件出错:", err)
		return
	}
	xmlFile.WriteString(xml.Header)
	encoder := xml.NewEncoder(xmlFile)
	encoder.Indent("", "\t")
	err = encoder.Encode(&post)
	if err != nil {
		fmt.Println("编码xml到文件出错:", err)
		return
	}

JSON生成与解析

对于JSON,我们先来看如何创建。同样,我们可以用marshal方式或者encoder方式。

Marshal方式生成

下面是marshal方式生成JSON,不过我们先需要适当改造前述代码中的结构体,使它们符合JSON标签的格式(JSON标签比xml简单些,也不需要头部声明,本身结构体赋值还和JSON数据很像):

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
)

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  Author `json:"author"`
}

func main() {
	post := Post{
		Id:      1,
		Content: "你好,Golang",
		Author: Author{
			Id:   2,
			Name: "张三",
		},
		Comments: []Comment{
			{
				Id:      1,
				Content: "C++才是好语言",
				Author: Author{
					Id:   3,
					Name: "李四",
				},
			},
			{
				Id:      2,
				Content: "Rust比C++好",
				Author: Author{
					Id:   4,
					Name: "王五",
				},
			},
		},
	}
	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		fmt.Println("列集到JSON时出错:", err)
		return
	}
	err = ioutil.WriteFile("post.json", output, 0644)
	if err != nil {
		fmt.Println("写入JSON到文件出错:", err)
		return
	}
}

encoder方式生成

使用编码方式生成JSON的代码:

	jsonFile, err := os.Create("post.json")
	if err != nil {
		fmt.Println("创建JSON文件出错:", err)
		return
	}
	encoder := json.NewEncoder(jsonFile)
	encoder.SetIndent("", "\t")
	err = encoder.Encode(&post)
	if err != nil {
		fmt.Println("编码JSON到文件出错:", err)
		return
	}

解析JSON也是两种方式:unmarshal或decoder。一般地Body,对于流式输入(如http.Request的),用解码方式较好;对于字符串或者内存的JSON数据,用unmarshal更好。

Unmarshal方式解析

// .................................................
func main() {
	jsonFile, err := os.Open("post.json")
	if err != nil {
		fmt.Println("打开JSON文件出错:", err)
		return
	}
	defer jsonFile.Close()
	jsonData, err := ioutil.ReadAll(jsonFile)
	if err != nil {
		fmt.Println("读取JSON数据出错:", err)
		return
	}

	var post Post
	json.Unmarshal(jsonData, &post)
	fmt.Println(post)
}

decoder方式解析

// ..............................................
	defer jsonFile.Close()
	decoder := json.NewDecoder(jsonFile)
	for {
		var post Post
		err := decoder.Decode(&post)
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("解码JSON出错:", err)
			return
		}
		fmt.Println(post)
	}
}

Web服务

下面我们仅就posts表来编写web服务:需要准备好mariadb 10.3数据库,另外,基本的表操作代码基本上是拷贝 golang web学习随便记4-内存、文件、数据库_sjg20010414的博客-CSDN博客 一文中的,只是 Post 结构体添加JSON标签。下面是main包中的data.go文件:

package main

import (
	"database/sql"
	"log"

	_ "github.com/go-sql-driver/mysql"
)

type Post struct {
	Id      int    `json:"id"`
	Content string `json:"content"`
	Author  string `json:"author"`
}

var Db *sql.DB

func init() { // 此 init 不显式调用,自动隐式调用,实现初始化全局变量 Db
	var err error
	Db, err = sql.Open("mysql", "gwp:dbpassword@tcp(172.17.0.1:3306)/gwp?charset=utf8mb4,utf8")
	if err != nil {
		panic(err)
	}
}

func Posts(limit int) (posts []Post, err error) {
	rows, err := Db.Query("SELECT id, content, author FROM posts LIMIT ?", limit) // Query(..) 预期返回多行结果集
	if err != nil {
		return
	}
	for rows.Next() { // 用循环遍历多行结果集
		post := Post{}
		err = rows.Scan(&post.Id, &post.Content, &post.Author) // Scan(..) 将结果集当前列值绑定到变量
		if err != nil {
			return
		}
		posts = append(posts, post) // 结果依次放入切片 posts
	}
	rows.Close() // 关闭结果集,清理内存
	return
}

func GetPost(id int) (post Post, err error) {
	post = Post{}
	err = Db.QueryRow("SELECT id, content, author FROM posts WHERE id = ?", id). // QueryRow(..) 预期返回单行结果集
											Scan(&post.Id, &post.Content, &post.Author) // pgsql 可以 RETURNING id
	return
}

func (post *Post) Create() (err error) {
	sql := "INSERT INTO posts (content, author) VALUES (?, ?)"
	stmt, err := Db.Prepare(sql) // 对于插入使用准备语句
	if err != nil {
		log.Fatal(err)
		return
	}
	defer stmt.Close()

	// err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)  // pgsql 可以一步设置 post.id
	if result, err := stmt.Exec(post.Content, post.Author); err != nil { // Exec(..)返回执行结果
		log.Fatal(err)
	} else {
		if last_insert_id, err := result.LastInsertId(); err != nil {
			log.Fatal(err)
		} else {
			post.Id = int(last_insert_id) // Mariadb/MySQL应该支持执行结果的 LastInsertId()
		}
	}
	return
}

func (post *Post) Update() (err error) {
	_, err = Db.Exec("UPDATE posts SET content = ?, author = ? WHERE id = ?",
		post.Content, post.Author, post.Id)
	return
}

func (post *Post) Delete() (err error) {
	_, err = Db.Exec("DELETE FROM posts WHERE id = ?", post.Id)
	return
}

下面是main包中的main.go文件:

package main

import (
	"encoding/json"
	"net/http"
	"path"
	"strconv"
)

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8088",
	}
	http.HandleFunc("/post/", handleRequest)
	http.HandleFunc("/posts/", handleList)
	server.ListenAndServe()
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
	var err error
	switch r.Method {
	case "GET":
		err = handleGet(w, r)
	case "POST":
		err = handlePost(w, r)
	case "PUT":
		err = handlePut(w, r)
	case "DELETE":
		err = handleDelete(w, r)
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func handleGet(w http.ResponseWriter, r *http.Request) (err error) {
	id, err := strconv.Atoi(path.Base(r.URL.Path)) // 路径中最后一个元素转换成整数
	if err != nil {
		return
	}
	post, err := GetPost(id)
	if err != nil {
		return
	}
	output, err := json.MarshalIndent(&post, "", "\t")
	if err != nil {
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(output)
	return
}

func handlePost(w http.ResponseWriter, r *http.Request) (err error) {
	len := r.ContentLength
	body := make([]byte, len)
	r.Body.Read(body)
	var post Post
	json.Unmarshal(body, &post)
	err = post.Create()
	if err != nil {
		return
	}
	w.WriteHeader(200)
	return
}

func handlePut(w http.ResponseWriter, r *http.Request) (err error) {
	id, err := strconv.Atoi(path.Base(r.URL.Path)) // 路径中最后一个元素转换成整数
	if err != nil {
		return
	}
	post, err := GetPost(id)
	if err != nil {
		return
	}
	len := r.ContentLength
	body := make([]byte, len)
	r.Body.Read(body)
	json.Unmarshal(body, &post)
	err = post.Update()
	if err != nil {
		return
	}
	w.WriteHeader(200)
	return
}

func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {
	id, err := strconv.Atoi(path.Base(r.URL.Path)) // 路径中最后一个元素转换成整数
	if err != nil {
		return
	}
	post, err := GetPost(id)
	if err != nil {
		return
	}
	err = post.Delete()
	if err != nil {
		return
	}
	w.WriteHeader(200)
	return
}

func handleList(w http.ResponseWriter, r *http.Request) {
	posts, err := Posts(10)
	if err != nil {
		return
	}
	output, err := json.MarshalIndent(&posts, "", "\t")
	if err != nil {
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(output)
	return
}

从 docker 启动 mariadb数据库,go get github.com/go-sql-driver/mysql 添加依赖包,然后 go run .运行服务。我们用 curl 来测试各项服务:

sjg@sjg-PC:~$ curl -i -X POST -H "Content-Type: application/json" -d '{"content":"我的第一条帖子","author":"赵六"}' http://127.0.0.1:8088/post/
HTTP/1.1 200 OK
Date: Thu, 18 May 2023 08:50:42 GMT
Content-Length: 0
sjg@sjg-PC:~$ curl -i -X GET http://127.0.0.1:8088/posts/
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 18 May 2023 08:52:35 GMT
Content-Length: 316

[
        {
                "id": 1,
                "content": "你好, C++! 08:33:04",
                "author": "李四"
        },
        {
                "id": 2,
                "content": "你好, C++! 08:46:48",
                "author": "李四"
        },
        {
                "id": 3,
                "content": "你好, C++! 08:51:08",
                "author": "李四"
        },
        {
                "id": 4,
                "content": "我的第一条帖子",
                "author": "赵六"
        }
]
sjg@sjg-PC:~$ curl -i -X GET http://127.0.0.1:8088/post/4
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 18 May 2023 08:53:28 GMT
Content-Length: 70

{
        "id": 4,
        "content": "我的第一条帖子",
        "author": "赵六"
}
sjg@sjg-PC:~$  curl -i -X PUT -H "Content-Type: application/json" -d '{"content":"我的第一条帖子(修改版)","author":"赵六"}' http://127.0.0.1:8088/post/4
HTTP/1.1 200 OK
Date: Thu, 18 May 2023 08:55:08 GMT
Content-Length: 0
sjg@sjg-PC:~$ curl -i -X DELETE http://127.0.0.1:8088/post/3
HTTP/1.1 200 OK
Date: Thu, 18 May 2023 08:56:18 GMT
Content-Length: 0

最后的结果是:

sjg@sjg-PC:~$ curl -i -X GET http://127.0.0.1:8088/posts/HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 18 May 2023 08:56:59 GMT
Content-Length: 252

[
        {
                "id": 1,
                "content": "你好, C++! 08:33:04",
                "author": "李四"
        },
        {
                "id": 2,
                "content": "你好, C++! 08:46:48",
                "author": "李四"
        },
        {
                "id": 4,
                "content": "我的第一条帖子(修改版)",
                "author": "赵六"
        }
]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值