随着 REST API 的兴起,基本上已经前后端分离,更多的返回格式是 json 字符串,这里简单讨论下在 GoLang 中如何编码和解码 JSON 结构。
GoLang 提供了 encoding/json
的标准库用于 JSON 的处理,简单记录 GoLang 中使用 JSON 的常用技巧。
编码
在使用库进行序列化时,只有字段名为大写的才会被编码到 JSON 中,不同结构的数组需要使用 []interface{}
进行转换,如下列举常用的表示方法。
package main
import (
"encoding/json"
"log"
)
type Account struct {
Email string
password string
Money float64 `json:"money,omitempty,string"`
Secret string `json:"-"`
}
type User struct {
Name string
Age int
Roles []string
Skill map[string]float64
Account Account
Extra []interface{}
Level map[string]interface{}
}
func main() {
skill := make(map[string]float64)
skill["python"] = 99.5
skill["ruby"] = 80.0
extra := []interface{}{123, "hello world"}
level := make(map[string]interface{})
level["web"] = "Good"
level["server"] = 90
level["tool"] = nil
user := User{
Name: "foobar",
Age: 27,
Roles: []string{"Owner", "Master"},
Skill: skill,
Account: Account{
Email: "foobar@example.com",
password: "YOUR PASSWORD",
Money: 11.1,
Secret: "some info",
},
Extra: extra,
Level: level,
}
rs, err := json.Marshal(user)
if err != nil {
log.Fatalln(err)
}
log.Println(string(rs))
}
输出的内容为。
{
"Name":"foobar",
"Age":27,
"Roles":[
"Owner",
"Master"
],
"Skill":{
"python":99.5,
"ruby":80
},
"Account":{
"Email":"foobar@example.com",
"money":"11.1"
},
"Extra":[
123,
"hello world"
],
"Level":{
"server":90,
"tool":null,
"web":"Good"
}
}
还可以使用 MarshalIndent
设置对齐的方式。
常用示例
- 忽略空白,添加
omitempty
注释。 - 将数值设置为字符串,添加
string
注释。 - 忽略部分字段,将字段名称设置为
-
。
简单格式化
对一些简单返回,无需将数据映射为结构体,可以使用 map[string]interface{}
进行映射。
package main
import (
"encoding/json"
"fmt"
)
func main() {
/*
info, err := json.Marshal(map[string]interface{}{
"Name": "J.K.",
"Age": 56,
})
info, err := json.Marshal([]map[string]interface{}{
map[string]interface{}{
"Name": "J.K.",
"Age": 56,
},
map[string]interface{}{
"Name": "Shakespeare",
"Age": 78,
},
})
*/
data := []map[string]interface{}{}
data = append(data, map[string]interface{}{"Name": "J.K.", "Age": 56})
info, err := json.Marshal(data)
if err != nil {
fmt.Println("json marshal failed:", err)
return
}
fmt.Println(string(info))
}
omitempty
使用的时候一定要慎重,注意 go 语言中的判空条件。
有时候当有数据的时候需要嵌套数据,而正常则只有部分状态信息,例如如下。
type Result struct {
Data MyStruct `json:"data,omitempty"`
Status string `json:"status"`
Reason string `json:"reason"`
}
对于上述结构体,当 MyStruct 未赋值的时候,仍然会显示 "data":{}
,可以将其修改为 Data *MyStruct
指针。
解码
解码就是将 JSON 字符串反序列化为 GoLang 对象,在匹配字段时 大小写不敏感的,而且不会设置私有字段,如果有不匹配的字段则直接忽略。
package main
import (
"encoding/json"
"log"
)
type Account struct {
Email string `json:"email"`
Money float64 `json:"money"`
PassWord string `json:"password"`
Level int `json:"level,string"`
Secret string `json:"-"`
//password string `json:"password"` // NOT Work
}
var jsonString string = `{
"email": "foobar@example.com",
"password" : "YOUR PASSWORD",
"money" : 100.5,
"level" : "10",
"Unexists" : "Some mess fields"
}`
func main() {
account := Account{}
err := json.Unmarshal([]byte(jsonString), &account)
if err != nil {
log.Fatal(err)
}
log.Printf("%#v\n", account)
}
需要注意:
- 如果要求将字符串转换为数值,可以增加
string
标签,此时 JSON 中必须使用""
否则报错。 - 忽略字段同样使用
-
,不过此时会设置默认的初始值,int
为0
,string
为""
。
另外,除了使用上述的 json.Unmarshal
进行解码,还可以调用 json 的 NewDecoder()
构造一个 Decode
对象,然后使用这个对象的 Decode()
方法赋值给定义好的结构对象。
通常用在读取文件或者 HTTP 请求中,可以直接以流的方式进行解析,无需先读取所有内容,相比来说性能更好。
package main
import (
"encoding/json"
"fmt"
"os"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
var person Person
file, _ := os.Open("person.json")
json.NewDecoder(file).Decode(&person)
fmt.Println(person)
}
func HandleUser(w http.ResponseWriter, r *http.Request) {
var person Person
if err := json.NewDecoder(r.Body).Decode(&person); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
fmt.Println(person)
}
当然,即使是 string
类型也可以使用 strings.NewReader()
让字符串变成一个 Stream
对象。
动态类型 VS. 延迟解析
如果某个字段是允许字符串或者整形的,那么就可以先将字段定义为 interface{}
类型,并后续进行检查。
对于 UserName
字段只有在使用时,才会用到他的具体类型,因此可以延迟解析。使用 json.RawMessage
方式,将 json 的字串继续以 byte
数组方式存在。
package main
import (
"encoding/json"
"log"
"strings"
)
type User struct {
UserName json.RawMessage `json:"username"`
Password string `json:"password"`
Email string
Phone int64
}
var jsonString string = `{
"username": 12345678901,
"password": "YOUR PASSWORD"
}`
func main() {
str := strings.NewReader(jsonString) // io.Reader
usr := User{}
err := json.NewDecoder(str).Decode(&usr)
if err != nil {
log.Fatalf("Decode failed, %#v\n", err)
}
var email string
if err = json.Unmarshal(usr.UserName, &email); err == nil {
usr.Email = email
}
var phone int64
if err = json.Unmarshal(usr.UserName, &phone); err == nil {
usr.Phone = phone
}
log.Printf("User %#v\n", usr)
}
或者将其解析为 map[string][]map[string]interface{}
类型,然后再按需进行处理。
混合结构
可以在序列化和反序列化时临时指定,将两个结构体临时粘和或者拆分。
package main
import (
"encoding/json"
"log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
type Place struct {
City string `json:"city"`
Country string `json:"country"`
}
func main() {
val, _ := json.Marshal(struct {
*Person
*Place
}{&Person{
Name: "andy",
Age: 18,
}, &Place{
City: "ShangHai",
Country: "China",
}})
log.Printf("%#v\n", string(val))
var place Place
var person Person
json.Unmarshal(val, &struct {
*Person
*Place
}{&person, &place})
log.Printf("%#v\n", place)
log.Printf("%#v\n", person)
}
常用技巧
之前已经介绍了 JSON 格式化基本介绍,这里整理一些常见的技巧。
时间转换
实际上可以理解为如何修改某个字段的格式化方法,如下是默认的方法,默认采用的是 RFC3339Nano
格式,在 src/time/format.go
中有相关的介绍。
package main
import (
"encoding/json"
"fmt"
"time"
)
type Person struct {
CreateTime time.Time `json:"CreateTime"`
}
func main() {
out, _ := json.Marshal(Person{
CreateTime: time.Now(),
})
fmt.Println("Person:", string(out))
}
其会输出 Person: {"CreateTime":"2021-02-10T22:27:53.465405727+08:00"}
,如果要自定义格式化方法,那么可以将其定义为字符串,或者重载 MarsalJSON()
方法。
package main
import (
"encoding/json"
"fmt"
"time"
)
type Person struct {
CreateTime time.Time `json:"CreateTime"`
}
func (p Person) MarshalJSON() ([]byte, error) {
type Alias Person
return json.Marshal(&struct {
Alias
CreateTime string `json:"CreateTime"`
}{
Alias: (Alias)(p),
CreateTime: p.CreateTime.Format("2006/01/02 15:04:05"),
})
}
func main() {
out, _ := json.Marshal(Person{
CreateTime: time.Now(),
})
fmt.Println("Person:", string(out))
}
另外,还有一种类似,相比来说更为合理的方式。
package main
import (
"encoding/json"
"fmt"
"time"
)
type DateTime time.Time
const timeFormate = `"2006-01-02 15:04:05"`
func (t DateTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%s", time.Time(t).Format(timeFormate))), nil
}
func (t *DateTime) UnmarshalJSON(data []byte) error {
// now, err := time.Parse(timeFormate, string(data))
now, err := time.ParseInLocation(timeFormate, string(data), time.Local)
if err != nil {
return err
}
*t = DateTime(now)
return nil
}
type Person struct {
CreateTime DateTime `json:"CreateTime"`
}
func main() {
out, _ := json.Marshal(Person{
CreateTime: DateTime(time.Now()),
})
fmt.Println("Person:", string(out))
var p Person
var jsonStr string = `{"CreateTime":"2021-04-11 22:54:44"}`
err := json.Unmarshal([]byte(jsonStr), &p)
if err != nil {
fmt.Println(err)
}
fmt.Println("Person:", time.Time(p.CreateTime))
}
枚举类型
通常会通过整形表示某些状态信息,可以通过如下方式进行转换。
type UserType int
const (
UserTypeInternal UserType = iota
UserTypeAdmin
)
func (u UserType) MarshalText() ([]byte, error) {
switch l {
case UserTypeAdmin:
return []byte("admin"), nil
case UserTypeInternal:
return []byte("internal"), nil
default:
return []byte("unspecified"), nil
}
}
func (u *UserType) UnmarshalJSON(data []byte) error {
switch strings.ToLower(string(data)) {
case `"internal"`:
*u = UserTypeInternal
default:
*u = UserTypeAdmin
}
return nil
}