GoLang JSON 编码解码

2016-10-07 language golang

随着 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 设置对齐的方式。

常用示例

  1. 忽略空白,添加 omitempty 注释。
  2. 将数值设置为字符串,添加 string 注释。
  3. 忽略部分字段,将字段名称设置为 -

简单格式化

对一些简单返回,无需将数据映射为结构体,可以使用 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)
}

需要注意:

  1. 如果要求将字符串转换为数值,可以增加 string 标签,此时 JSON 中必须使用 "" 否则报错。
  2. 忽略字段同样使用 - ,不过此时会设置默认的初始值,int0string""

另外,除了使用上述的 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
}