golang WEB应用【2】:json数据处理应用 原创
https://blog.csdn.net/loo_Charles_ool/article/details/138916161原地址
2024-05-15 17:33:48阅读量980
收藏28
32赞 关注
文章目录
json数据处理应用
JSON格式简介
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它基于ECMAScript(欧洲计算机制造商协会指定的一种脚本程序标准)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得JSON成为理想的数据交换语言。它易于人阅读和编写,同时也易于机器解析和生成
JSON格式是键/值对的集合。在不同的编程语言中,这些键/值对被理解为对象(object)
、纪录(record)
、结构(struct)
、字典(dictionary)
、哈希表(hash table)
、有键列表(keyed list)
或者关联数组(associative array
golang 中主要理解为结构(struct)
和接口(interface)
JSON的语法规则包括:
- 数据在名称/值对中
- 数据由逗号分隔
- 花括号保存对象
- 方括号保存数组
JSON数据的结构有两种:
- 对象(Object):一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个名称后跟一个“:”(冒号);“‘名称/值’对”之间使用“,”(逗号)分隔。
- 数组(Array):值的有序集合。一个数组以"[“(左中括号)开始,”]"(右中括号)结束。值之间使用“,”(逗号)分隔。
JSON的值可以是以下几种类型:
- 字符串(String):以双引号包围的Unicode字符序列。
- 数值(Number):整数或浮点数。
- 对象(Object):上面描述的JSON对象。
- 数组(Array):上面描述的JSON数组。
- 布尔值(Boolean):true或false。
- 空值(Null):表示一个空值。
示例:
{
"name": "张三",
"age": 30,
"isMarried": false,
"children": null,
"hobbies": ["reading", "traveling", "sports"],
"address": {
"city": "北京",
"street": "长安街",
"postcode": "100000"
}
}
在这个例子中,我们定义了一个包含多种类型值的对象,其中包含了姓名、年龄、婚姻状态、爱好、子女和地址等信息
Go语言中的JSON序列化和反序列化
在Go语言中,encoding/json
标准库提供了JSON序列化和反序列化的功能。序列化(Serialization)是指将Go语言中的结构体(struct)或其他数据类型转换为JSON格式的字符串,而反序列化(Deserialization)则是将JSON格式的字符串转换回Go语言中的结构体或其他数据类型。
JSON序列化
序列化使用 json.Marshal
函数将Go语言的数据结构转换为JSON格式的字节切片(byte slice)
。
package main
import (
"encoding/json"
"fmt"
"log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Address string `json:"address,omitempty"` // omitempty 表示如果字段为空(默认值),则不包含在JSON中
}
func main() {
p := Person{
Name: "张三",
Age: 30,
Email: "[email protected]",
Address: "",
}
// 序列化
data, err := json.Marshal(p)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data)) // 输出:{"name":"张三","age":30,"email":"[email protected]
在上面的例子中,我们定义了一个 Person
结构体,并使用结构体标签(struct tags)
来指定JSON字段的名称。json.Marshal
函数将 Person
实例序列化为JSON格式的字节切片
,然后将其转换为字符串
并打印出来。
JSON反序列化
反序列化使用 json.Unmarshal 函数将JSON格式的字节切片转换为Go语言的数据结构。
package main
import (
"encoding/json"
"fmt"
"log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Address string `json:"address,omitempty"`
}
func main() {
jsonData := []byte(`{
"name": "张三",
"age": 30,
"email": "[email protected]"
}`)
var p Person
// 反序列化
err := json.Unmarshal(jsonData, &p)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", p) // 输出:{Name:张三 Age:30 Email:[email protected] Address:}
在这个例子中,我们定义了一个包含JSON字符串的 jsonData 变量,并使用 json.Unmarshal 函数将其反序列化为 Person 结构体的实例。反序列化成功后,我们可以打印出结构体的内容
注意事项
- 在结构体标签中,可以使用 omitempty 选项来忽略空值字段。
- 结构体中的字段名必须是大写开头的,否则在序列化和反序列化时不会被识别。
- 如果JSON中的字段名与结构体中的字段名不一致,可以使用结构体标签来映射字段名。
- 序列化和反序列化过程中可能会遇到多种错误,例如数据类型不匹配、字段不存在等,需要对这些错误进行处理。
JSON的映射
在上面的序列化和反序列化的例子当中,可以看到需要定义一些规则来指定如何将Go的结构体字段映射到JSON的键,以及如何将JSON的键映射回Go的结构体字段。Go在JSON的映射上,一般采用结构体(struct)
和接口(interface{})
来进行映射,接下来会分别介绍
结构体映射
结构体在处理JSON的映射过程中,一般采用结构体标签(Struct Tags
,结构体标签是附加在结构体字段后面的字符串,它们提供了在序列化和反序列化过程中如何处理字段的额外信息。结构体标签通常由一个或多个键值对组成,键和值之间用逗号分隔,键值对之间用空格分隔。在处理JSON时,json键用于指定JSON字段的名称和其他选项。
常用结构体标签选项
- json:“name”: 指定JSON字段的名称。如果省略,则默认使用结构体字段的名称。
- omitempty: 如果字段的值为空(例如,字符串为空、数字为0、布尔值为false、指针或接口为nil),则在序列化时省略该字段。这个选项通常与json:"name"一起使用,如json:“name,omitempty”
- -: 表示该字段在序列化和反序列化过程中应该被忽略
示例
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address,omitempty"`
}
func main() {
p := Person{
Name: "张三",
Age: 30,
Address: "",
}
// 序列化
data, err := json.Marshal(p)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data)) // 输出:{"name":"张三","age":30}
// 反序列化
jsonData := []byte(`{"name":"李四","age":25,"address":"北京"}`)
var p2 Person
err = json.Unmarshal(jsonData, &p2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", p2) // 输出:{Name:李四 Age:25 Address:北京}
在这个示例中,我们定义了一个Person结构体,其中包含三个字段,每个字段都有自己的结构体标签。在序列化时,由于Address字段的值为空,并且标签中包含了omitempty选项,所以该字段被省略了。在反序列化时,JSON数据中的name、age和address键被映射回结构体中的Name、Age和Address字段
在go1.18及之后的版本,引入了泛型的概念,可以通过泛型来编写更灵活和可重用的代码。创建能够处理任何类型的JSON数据的函数
package main
import (
"encoding/json"
"fmt"
"log"
)
// 泛型函数,用于解析JSON到任意类型的对象
func ParseJSON[T any](data []byte, obj T) (T, error) {
err := json.Unmarshal(data, &obj)
if err != nil {
return obj, err
}
return obj, nil
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
func main() {
// JSON数据
jsonData := []byte(`{
"name": "张三",
"age": 30,
"email": "[email protected]"
}`)
// 使用泛型函数解析JSON
var person Person
person, err := ParseJSON[Person](jsonData, person)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", person) // 输出:{Name:张三 Age:30 Email:[email protected]}
}
在这个示例中,我们定义了一个泛型函数 ParseJSON,它接受任意类型的对象作为参数,并将JSON数据解析到该对象中。我们使用类型参数 T 来表示任意类型,并通过泛型函数来解析JSON数据到一个 Person 对象中。
接口映射
结构体映射,比较适合固定和已知的JSON结构,我们可以看上面代码中使用的例子
{
"name":"张三",
"ag
其对应的结构体
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
如果多添加一个address后
{
"name":"李四",
"age": 25,
"address":"北京"
上面的结构体显然无法解析,当然也可以添加一个address字段来解析,但这需要建立在完全了解接收的JSON数据的结构才可以,有很多情况我们可能并不能完全确定数据结构,需要更灵活的方式来处理,一般会使用到接口(interface{})
interface{} 类型在Go中是一个空接口,它可以表示任何类型。这意味着你可以使用 interface{} 来解析任何JSON数据,而不需要预先定义一个具体的结构体。这在处理未知JSON结构或动态字段时非常有用。
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
// JSON数据
jsonData := []byte(`{
"name": "张三",
"age": 30,
"email": "[email protected]"
}`)
// 使用map[string]interface{}来解析JSON
var result map[string]interface{}
err := json.Unmarshal(jsonData, &result)
if err != nil {
log.Fatal(err)
}
// 访问解析后的数据
name, ok := result["name"].(string)
if ok {
fmt.Println("Name:", name)
}
age, ok := result["age"].(float64)
if ok {
fmt.Println("Age:", age)
}
email, ok := result["email"].(string)
if ok {
fmt.Println("Email:", email)
}
}
说明:可以看到在获取数据时使用了类型转换的句子来断言现在获取的类型
name, ok := result["name"].(string)
,这里ok
返回的是bool
类型,表示是否存在这个name
键
在这个示例中,我们使用 map[string]interface{}
来解析JSON数据。这允许我们动态地访问JSON对象中的字段,而不需要预先定义一个结构体。我们通过类型断言来获取字段的特定类型值。
设计RESTful API的原则
RESTful API是一种遵循REST(Representational State Transfer)架构风格的网络应用程序接口。REST是一种设计网络服务的架构风格,它定义了一组用于创建分布式系统的原则和约束。RESTful API通常使用HTTP协议作为底层传输协议,并利用HTTP的方法(如GET、POST、PUT、DELETE等)来对资源进行操作
设计RESTful API的时候,一般遵循一定的原则:
- 资源导向:RESTful API应该以资源为中心,每个资源都应有唯一的URL标识。资源通常与数据库中的表或实体对应
- 无状态:API应该是无状态的,这意味着每个请求都应该包含所有必要的信息,服务器不应该存储任何关于客户端状态的信息。
- 统一接口:API应该有一套统一的接口,使用标准的HTTP方法(如GET、POST、PUT、DELETE等)来进行资源的CRUD(创建、读取、更新、删除)操作。
- URL命名规范:URL应该清晰地反映资源结构,使用名词而不是动词,并且应该使用复数形式。
- 状态码使用:应该使用适当的HTTP状态码来表示请求的结果,例如200表示成功,201表示创建成功,404表示未找到资源等。
- 数据格式:API应该返回结构化的数据格式,通常是JSON或XML。JSON由于其在Web中的普遍使用和轻量级特性,通常是首选。
- 过滤、排序和分页:提供参数来允许客户端对返回的数据进行过滤、排序和分页,以便于用户根据需要获取数据。
- 错误处理:应该提供有用的错误信息,以便于开发者调试和理解问题。错误信息应该清晰、具体,并且不应该暴露后端实现的细节。
- 安全性:API应该考虑到安全性,使用HTTPS来保护数据传输,使用适当的认证和授权机制来保护资源。
- 限流和缓存:为了防止滥用和提高性能,应该实现限流和缓存机制。
- 版本控制:随着API的发展,可能需要更改现有的API。为了不破坏现有的客户端,应该实现版本控制,以便于平滑过渡。
- 文档:提供详尽的文档是至关重要的,它应该包括API的每个端点的详细描述、参数、请求和响应示例等。
- 可测试性:API应该易于测试,可以通过单元测试、集成测试和端到端测试来确保其可靠性和稳定性。
- 可扩展性:设计时应该考虑到未来的扩展,API应该能够适应更多的资源和功能而无需大规模重构
实现和使用API
RESTful API实现
下面的简单例子我们单纯使用net/http
来实现一个简单的RESTful API,包含了GET,PUT,POST,DELETE四种请求方式。如果使用现有的框架如Gin、Beego、Echo等(这些框架后面会再介绍),会更加方便,灵活,但框架也是基于net/http
实现的
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
)
// 定义一个简单的产品结构体
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
}
// 创建一个产品切片,模拟数据库中的数据
var products = []Product{
{ID: 1, Name: "苹果", Price: 50},
{ID: 2, Name: "香蕉", Price: 30},
{ID: 3, Name: "橙子", Price: 40},
}
// 处理POST请求,创建新产品
func createProduct(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var product Product
_ = json.NewDecoder(r.Body).Decode(&product)
products = append(products, product)
json.NewEncoder(w).Encode(product)
}
// 处理GET请求,获取所有产品
func getProducts(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
json.NewEncoder(w).Encode(products)
}
// 处理GET请求,获取单个产品
func getProduct(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
id := r.URL.Query().Get("id")
for _, product := range products {
if fmt.Sprintf("%d", product.ID) == id {
json.NewEncoder(w).Encode(product)
return
}
}
http.Error(w, "Product not found", http.StatusNotFound)
}
// 处理PUT请求,更新产品
func updateProduct(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "Invalid product ID", http.StatusBadRequest)
return
}
var product Product
_ = json.NewDecoder(r.Body).Decode(&product)
product.ID = id
for i, p := range products {
if p.ID == id {
products[i] = product
json.NewEncoder(w).Encode(product)
return
}
}
http.Error(w, "Product not found", http.StatusNotFound)
}
// 处理DELETE请求,删除产品
func deleteProduct(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "Invalid product ID", http.StatusBadRequest)
return
}
for i, p := range products {
if p.ID == id {
products = append(products[:i], products[i+1:]...)
break
}
}
json.NewEncoder(w).Encode(map[string]string{"message": "Product deleted"})
}
func main() {
http.HandleFunc("/products", getProducts)
http.HandleFunc("/product", getProduct)
http.HandleFunc("/createProduct", createProduct)
http.HandleFunc("/updateProduct", updateProduct)
http.HandleFunc("/deleteProduct", deleteProduct)
log.Println("Server started on http://localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(
在这个例子中,定义了5个路由以及函数
/product
:getProducts函数处理GET请求,用于获取所有产品/products
:getProduct函数处理GET请求,用于获取单个产品/createProduct
:createProduct函数处理POST请求,用于创建新产品/updateProduct
:updateProduct函数处理PUT请求,用于更新产品信息/deleteProduct
:deleteProduct函数处理DELETE请求,用于删除产品
请求示例及结果
调用RESTful API
调用RESTful API依然是使用net/http
,我们在前面的章节已经有过简单介绍。在下面的例子中,我们将使用一个名为https://jsonplaceholder.typicode.com/的公开API,该API提供了一系列模拟的RESTful API端点。
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
// 定义一个请求结构体
type Request struct {
Method string `json:"method"`
Url string `json:"url"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
}
// 处理GET请求
func handleGet(w http.ResponseWriter, r *http.Request) {
// 解析请求参数
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
id := r.URL.Query().Get("id")
// 构建请求
request := Request{
Method: "GET",
Url: fmt.Sprintf("https://jsonplaceholder.typicode.com/todos/%s", id),
Headers: map[string]string{
"Content-Type": "application/json",
},
}
// 发起请求
response, err := http.NewRequest("GET", request.Url, nil)
if err != nil {
log.Fatal(err)
}
for key, value := range request.Headers {
response.Header().Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(response)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// 读取响应体
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
// 输出响应体
fmt.Fprintf(w, "%s", body)
}
func main() {
http.HandleFunc("/get", handleGet)
log.Println("Server started on http://localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
在这个示例中,我们定义了一个Request结构体,用于存储请求的方法、URL、头信息和正文。我们创建了一个handleGet函数,它解析请求参数,构建请求,并发起GET请求。然后,我们使用ioutil.ReadAll函数读取响应体,并将其输出到客户端。
整个例子相当于创建了一个api转发,可以尝试在里面添加一些中间件将获取到的数据存储,清洗再进行优化展示。
标签:web,http,string,err,JSON,json,go,序列化 From: https://www.cnblogs.com/cheyunhua/p/18449342