golang json unmarshal 踩坑

最近在工作上遇到一个关于 json unmarshal 的问题。

参考文章

人在家中坐,锅从天上来

某天,组里的小哥对我说,某个服务的 function test failed,可能是与我的最近一次代码改动有关。我有点震惊,就那小改动,居然会影响。既然锅都来了,那就看看我的代码,看看是不是真的有问题。
首先,我们定位到 test failed 的地方,TestUpdateDriverTier function 失败了。接着我们去 kibana 看看 log, 发现错误是

1
error=no tier config found for cityID : 1 and taxiTypes [233]

既然日志说没有 no found,我又去检查配置中心,结果配置中心是有对应的 json 配置的。

坑一

我随后又再看了一眼 function test ,离谱的地方是 TestUpdateDriverTier 失败了,但是 TestBulkUpdateDriverTier 成功了。我检查了两个代码的区别:UpdateDriverTier 和 BulkUpdateDriverTier 的唯一不同是:
BulkUpdateDriverTier 是一个 for 循环单个更新,而 UpdateDriverTier 是只有一个更新。两者背后调用的 function 都是一样了,这就使我百思不得其解了: 为什么只有 TestUpdateDriverTier 失败了。

难上加难的地方在于:function test 不像 unit test 可以轻松的在本地测试,function test 需要在远程服务器上测试。为了复现这个问题, 我在自己的电脑上调用那个 API, 结果还真就只有 UpdateDriverTier 报错误, BulkUpdateDriverTier 没有报错。

踩坑一是因为我自己检查代码是时候不够仔细, UpdateDriverTier 和 BulkUpdateDriverTier 还有一个巨大的区别。

1
2
3
4
5
6
7
8
9
10
11
// UpdateDriverTier ...
func (s *Service) UpdateDriverTier(ctx context.Context, driverTier *dto.DriverTier) error {

if err := s.TierCalculator.ForceUpdateTier(ctx, driverTier.DriverID, driverTier.Progress); err != nil {
log.Error(logkey.Error, "error in force update driver tier", logkey.DriverID, driverTier.DriverID, logkey.Error, err)
s.statsClient.Count1(statsdkey.ServiceContextTag, updateDriverTierTag, statsdkey.FailedTag)
return err
}

return nil
}

结果发现 BulkUpdateDriverTier 是异步的,即使有错误,gorutine 也不会返回到 main gorutine,所以 API 得到的结果是没有错误的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BulkUpdateDriverTier ...
func (s *Service) BulkUpdateDriverTier(ctx context.Context, driverTiers []*dto.DriverTier) error {

go func(driverTiers []*dto.DriverTier) {
for _, driverTier := range driverTiers {
if err := s.TierCalculator.ForceUpdateTier(ctx, driverTier.DriverID, driverTier.Progress); err != nil {
log.Warn(logkey.Message, "error in bulk force update driver", logkey.DriverID, driverTier.DriverID, logkey.Error, err)
s.statsClient.Count1(statsdkey.ServiceContextTag, bulkUpdateDriverTierTag, statsdkey.FailedTag)
}
}
}(driverTiers)

return nil
}

坑二

其实我在最开始的时候,是没有发现坑一的。我为了搞清楚为什么找不到配置,我就临时添加了代码打 log,把那个配置的变量给打印出来。结果发现了配置的 point nil

1
feiyang="&{0 0 0 false <nil> map[] 0}"

我又换了一个输入,正常的配置应该是这个样子的
1
feiyang="&{0 0 0 true 0xc0214e8e00 map[41:0xc01627b810 42:0xc01627b880 43:0xc01627b8f0 44:0xc01627b9d0] 0}"

我就开始搜索这个错误,发现一个类似的:反序列化Map或Slice的类型要求要与 序列化时Map或Slice的类型必须保持一致

差不多就是下面这个例子,golang struct 里面要求的是 Rank int,结果 json 传入的是 string,最终 Unmarshal 出错,得到默认初始值的 struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"encoding/json"
"fmt"
)

// TierRules ...
type TierRules struct {
Rank int64 `json:"rank"`
TierID int64 `json:"tierID"`
UseAsDefault bool `json:"useAsDefault"`
}

func main() {

tier := TierRules{
Rank: 1,
TierID: 1,
UseAsDefault: true,
}

tierjson, err := json.Marshal(tier)

if err != nil {
fmt.Println(err)
}
fmt.Printf("tierjson type is: %T\n", tierjson)
fmt.Printf("tierjson value is: %v\n", string(tierjson))

var tier1 TierRules
str := "{\"rank\":\"1\",\"tierID\":\"1\",\"useAsDefault\":true}"
fmt.Printf("str value is: %v\n", str)
err = json.Unmarshal([]byte(str), &tier1)
if err != nil {
fmt.Println("反序列化错误:", err)
}
fmt.Printf("tier1 type is: %T\n", tier1)
fmt.Printf("tier1 value is: %v\n", tier1)

}

运行结果如下

1
2
3
4
5
6
7
8
tierjson type is: []uint8
tierjson value is: {"rank":1,"tierID":1,"useAsDefault":true}

str value is: {"rank":"1","tierID":"1","useAsDefault":true}
反序列化错误: json: cannot unmarshal string into Go struct field TierRules.rank of type int64

tier1 type is: main.TierRules
tier1 value is: {0 0 true}

解决

我太菜了,搞了 2 天终于发现是因为配置 json 文件里面数据类型不一致导致的问题后,再次去配置中心检查,在密密麻麻的配置中,终于发现了那个错误(rank 是 string 类型)。下一步是:我们如何在配置的时候就就行校验,提前预防这样的错误呢?