小谈Go中易出错的点:1、json unmarshal

181 阅读2分钟

Question

下面的测试能够通过吗?

func TestJsonUnmarshal(t *testing.T) {
	var err error
	p := Person{
		Name: "zm",
		Age:  29,
		Sex:  1,
	}

	t.Run("empty json unmarshal to value struct", func(t *testing.T) {
		data := []byte(`{}`)
        // notice we unmarhsal data to `p`
		err = json.Unmarshal(data, &p)
		assert.Nil(t, err)

		assert.Equal(t, "", p.Name)
		assert.Equal(t, 0, p.Age)
		assert.Equal(t, int8(0), p.Sex)
	})
}

主要逻辑是将一个空的json字符串{} unmarshal到一个已经有值的Person struct p 上。

事出无常必有妖,既然我这么问了,答案是上面的测试通过不了的

难道是我对json包的Unmarshal有误解吗?

按照之前的使用情况,我默认空的json字符串应该覆盖struct,就像测试中assert中的那样,所有字段值变成该字段类型的zero value才对。

但与我最初想的完全不同——Unmarshal时只会覆盖有值的字段。

var err error
p := Person{
	Name: "zm",
	Age:  29,
	Sex:  1,
}

t.Run("x", func(t *testing.T) {
	// notice name becomes empty; Age 29->30; sex:1->2
	data := []byte(`{"Age":30,"Sex":2}`)
	err = json.Unmarshal(data, &p)
	assert.Nil(t, err)

	assert.Equal(t, "zm", p.Name) 	// right
	assert.Equal(t, 30, p.Age)    	// right 
	assert.Equal(t, int8(2), p.Sex) // right 
})

Dive into

上面描述的情况或许只是当初设计Unmarshal方法时的约定,不算是bug

dive into就算了,各位如果有兴趣,就留待各位自己去探索吧~

Background

上面是我在开发功能时实际遇到的问题。

简单来说,某些情况下,我们通过下发故障信息到用户的机器,业务代码中集成我们的sdksdk中循环获取故障信息,并且规定,发送空字符串{}表示清空故障信息,不再放火。

伪代码如下:

var param Param // 存放放火配置
for {
  config := apollo.GetConfig()
  // Boom !!!
  if len(config) != 0 {
    _ = json.Unmarshal(config, &param)
  }
  time.Sleep(5*time.Second)
}

上面的代码不会达到我们预期的结果。

Inspiration

给我们的启示是什么,我觉得至少有两个。

  • 每次使用json.Unmarshal反序列化字符串时,我们一定要使用新创建的struct作为载体,否则很容易出现问题。

让我莫名想起来sliceappend操作,最佳实践是使用同名的slice作为append的返回值。

arr = append(arr, 1) // that's fine
append(arr, 2) // oh no, it may be in trouble
  • 全局变量,它啊,真的很容易引发问题。
  • 单元测试,能够发现很多始料不及的问题。

Reference

滴滴混沌工程项目代码