在golang里经常需要将数据在struct对象和json串之间转换。

golang中struct和json串互相转化.png

最简单的序列化

序列化(从golang对象转换为JSON串)只需要对结构体进行标注即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 对struct进行标注
type User struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
}

// 序列化
if buf, err := json.Marshal(User{
    Name:     "seedjyh",
    Email:    "seedjyh@gmail.com",
    Password: "123456",
}); err != nil {
    fmt.Println(string(buf))
    // 输出 {"name":"seedjyh","email":"seedjyh@gmail.com","password":"123456"}
}

很简单不是吗?


追加JSON键

然而,如果需要将User对象传递给服务器时,服务器需要追加一个current键值对,以存储发送时刻,那该怎么办?

方法1:直接修改原结构体

直接往User结构体里添加一个Current字段,几下敲完。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type User struct {
    Name     string    `json:"name"`
    Email    string    `json:"email"`
    Password string    `json:"password"`
    Current  time.Time `json:"current"`  // 加了这一行
}
// 序列化
if buf, err := json.Marshal(User{
    Name:     "seedjyh",
    Email:    "seedjyh@gmail.com",
    Password: "123456",
    Current:  time.Now(),
}); err != nil {
    fmt.Println(string(buf))
    // 输出 {"name":"seedjyh","email":"seedjyh@gmail.com","password":"123456","current":"2020-03-12T14:55:42.7560612+08:00"}
}

看上去不错,但是这个Current字段会影响所有用到这个User结构的地方。初始化的时候麻烦,序列化(而不需要Current字段)的地方更麻烦。

有更好的方法吗?

方法2:定义一个新的结构体

1
2
3
4
5
6
type UserWithCurrent struct {
    Name     string    `json:"name"`     // 照抄
    Email    string    `json:"email"`    // 照抄
    Password string    `json:"password"` // 照抄
    Current  time.Time `json:"current"`  // 新增字段
}

这比方法1好一些。只在序列化的地方定义UserWithCurrent结构,其他代码就不受干扰了。

不过还是有两个缺点:

  1. 构造UserWithCurrent对象时,需要将User对象的每个字段逐一赋值,麻烦又易错。
  2. 如果原来的User对象增加了字段(比如Age),这里UserWithCurrent也得跟着改。

有更好的方法吗?

方法3:结构体组合

golang没有提供面向对象的继承功能,但提供了一个用组合实现继承的方法。

1
2
3
4
type UserWithCurrent struct {
    *User                              // 不加星号也可以,加星号可以节约内存开销
    Current time.Time `json:"current"` // 新增字段
}

这种结构的UserWithCurrent不但能实现添加current字段的要求,在用User对象构造UserWithCurrent对象时,可以直接将User对象赋值过去而不需对User对象的字段逐个赋值。如下所示:

1
2
3
4
userToSend := UserWithCurrent{
    User:    &user,  // user是User结构体的对象。
    Current: time.Now(),
}

隐藏JSON键

现在有个需求:将User对象(最初的那个struct)的内容按照JSON格式打印到日志里。但是由于Password是敏感字段,需要在打印时去掉。也就是原来的Name、Email、Password三个字段只打印前两个。

是去掉一个键,而不是新增。该怎么办?

方法1:定义一个新的结构体!

1
2
3
4
5
type UserWithoutPassword struct {
    Name     string    `json:"name"`     // 照抄
    Email    string    `json:"email"`    // 照抄
    // Password去掉了。
}

然后在构造这个对象时逐个从user对象拷贝字段,User字段发生变化时也得连锁修改UserWithoutPassword结构。麻烦,易错。

只有这个方法吗?

并不是。

方法2:结构体组合+重名标注

用结构体组合,依然可以实现字段

1
2
3
4
5
6
7
8
9
type UserWithoutPassword struct {
    *User
    Abc int `json:"password,omitempty"` // 关键是标注password和omitempty
}

userWithoutPassword := UserWithoutPassword{
    User: &user,
    // 字段Abc留空,不赋值
}

稍微解释一下:

UserWithoutPassword里新增的一行里,字段名Abc和字段类型int可以随便写,关键是标注。

Abc的标注为password,和User对象里的Password字段的标注相同。此时,在json包对对象进行序列化时,JSON的password键的值只看外层的struct字段。

golang中json序列化时的键覆盖.png

也就是说,最后序列化的时候,password键的值由UserWithoutPassword的Abc字段决定。

然后在Abc的标注里加上omitempty,并在构造UserWithoutPassword对象时故意不对Abc赋值(让其保持默认的空值),即可在最终生成的JSON串里隐藏password这个JSON键。


重命名JSON键

还是原来那个User结构,现在要求name键改成username键。

golang中json序列化时的键重命名.png

其实也很简单,联合使用上面的“追加JSON键”和“隐藏JSON键”即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type UserRenameKey struct {
    *User
    Abc int    `json:"name,omitempty"` // 覆盖键name
    Def string `json:"username"`       // 新键username
}

userRenameKey := UserRenameKey{
    User: &user,
    // 字段Abc留空,不赋值
    Def:  user.Name, // 将user对象里的Name字段赋值给Def字段,最后序列化成username
}

参考资料