Go 如何解析 json 內部結構不確定的情況

Go 如何解析 json 內部結構不確定的情況

文是透過組織曾經梳理過的一篇文章和一個問答而成。主要介紹的是關於 Go 如何解析 json 內部結構不確定的情況。

問題描述

這或許是新手常會遇到的一個問題,無論是在各種微信群、知乎、思否、stackoverflow 上,我常會遇到這樣的提問。

什麼問題呢?直接看一個來自思否上的提問吧。內容如下:

上游傳遞不確定 的json ,如何透傳給下游業務?比如,我解析引數

{ “test”: 1, “key”: { “k1”: “1”, “k2”: 2 }}

但是key 結構體下面是未知的。可能是K1 K2 K3 。。。 KN。如何解析傳遞那?

如何處理

對於 json 格式資料的解析,如果其中的某個成員結構不確定,我總結一般有幾種方式處理。

第一種,最容易想到的就是,將那個不確定的成員用 map[string]interface{} 替代。

type Data struct { Test int `json:“test”` Key map[string]interface{} `json:“test”`}

但問題是,這種方式太坑,每次從 key 中拿資料,都要做型別檢查,判斷是否 ok。

第二種,既然 map[string]interface{} 的方式太坑,那如果要是能用結構體就好了,雖然其中某個成員的結構不確定,但如果共性欄位比較多,比如都是與人相關,那肯定都有名字,年齡之類的欄位,但如果是教師和學生,就會有一些不同的欄位,把所有的不同欄位都包含進來即可。但如果不同欄位太多,那也不是很方便。

第三種,終極解決方案,如果能先解析第一層的結構,再根據第一層的結果,確定第二層的結構,那就方便多了。不確定的成員依然用 map[string]interface{} 表示,確定結構後,再將 map[string]interface{} 解析為具體的某個結構。結構體使用起來就方便很多了。

問題最終就變成了如何將 map[string]interface{} 轉化為 struct,這個過程必然會用到反射,可以自己實現。但其他人早造就想到了,一個第三方庫,地址:https://github。com/mitchellh/mapstructure 。

一個實際的案例

看一個實際的案例。

近期工作中,因為要把資料庫資料實時更新到 elasticsearch,在實踐過程中遇到了一些 JSON 資料處理的問題。

什麼樣的資料呢?

實時資料獲取是透過阿里開源的 canal 元件實現的,並透過訊息佇列 kafka 傳輸給處理程式。我們將接收到的 JSON 資料類似如下的形式。

{ “type”: “UPDATE”, “database”: “blog”, “table”: “blog”, “data”: [ { “blogId”: “100001”, “title”: “title”, “content”: “this is a blog”, “uid”: “1000012”, “state”: “1” } ]}

簡單說下資料的邏輯,type 表示資料庫事件是新增、更新還是刪除事件,database 表示對應的資料庫名稱,table 表示相應的表名稱,data 即為資料庫中資料。

怎麼處理這串 JSON 呢?

json 轉化為 map

最先想到的方式就是透過 json。Unmarshal 將 JSON 轉化 map[string]interface{}。

示例程式碼:

func main () { msg := []byte(`{ “type”: “UPDATE”, “database”: “blog”, “table”: “blog”, “data”: [ { “blogId”: “100001”, “title”: “title”, “content”: “this is a blog”, “uid”: “1000012”, “state”: “1” } ]}`) var event map[string]interface{} if err := json。Unmarshal(msg, &event); err != nil { panic(err) } fmt。Println(event)}

列印結果如下:

map[data:[map[title:title content:this is a blog uid:1000012 state:1 blogId:100001]] type:UPDATE database:blog table:blog]

到此,就成功解析出了資料。接下來是使用它,但我覺得 map 通常有幾個不足。

透過 key 獲取資料,可能出現不存在的 key,為了嚴謹,需要檢查 key 是否存在;

相對於結構體的方式,map資料提取不便且不能利用 IDE 補全檢查,key 容易寫錯;

針對這個情況,可以怎麼處理呢?如果能把 JSON 轉化為struct 就好了。

json 轉化為 struct

在 GO 中,json 轉化為 struct 也非常方便,只需提前定義好轉化的 struct 即可。我們先來定義一下轉化的 struct。

type Event struct { Type string `json:“type”` Database string `json:“database”` Table string `json:“table”` Data []map[string]string `json:“data”`}

說明幾點

實際場景中,canal 訊息的 data 結構是由表決定的,在 JSON 成功解析前無法提前知道,所以這裡定義為 map[string]string;

轉化的結構體成員必須是可匯出的,所以成員變數名都是大寫,而與 JSON 的對映透過 json:“tagName” 的 tagName 完成。

解析程式碼非常簡單,如下:

e := Event{}if err := json。Unmarshal(msg, &e); err != nil { panic(err)}fmt。Println(e)

列印結果:

{UPDATE blog blog [map[blogId:100001 title:title content:this is a blog uid:1000012 state:1]]}

接下來,資料的使用就方便了不少,比如事件型別獲取,透過 event。Type 即可完成。不過,要潑盆冷水,因為 data 還是 []map[string]string 型別,依然有 map 的那些問題。

能不能把 map 轉化為 struct ?

map 轉化為 struct

據我所知,map 轉為轉化為 struct,GO 是沒有內建的。如果要實現,需要依賴於 GO 的反射機制。

不過,幸運的是,其實已經有人做了這件事,包名稱為

mapstructure

,使用也非常簡單,敲一遍它提供的幾個

例子

就學會了。README 中也說了,該庫主要是遇到必須讀取一部分 JSON 才能知道剩餘資料結構的場景,和我的場景如此契合。

安裝命令如下:

$ go get https://github。com/mitchellh/mapstructure

開始使用前,先定義 map 將轉化的 struct 結構,即 blog 結構體,如下:

type Blog struct { BlogId string `mapstructure:“blogId”` Title string `mapstructrue:“title”` Content string `mapstructure:“content”` Uid string `mapstructure:“uid”` State string `mapstructure:“state”`}

因為,接下來要用的是 mapstructure 包,所以 struct tag 標識不再是 json,而是 mapstructure。

示例程式碼如下:

e := Event{}if err := json。Unmarshal(msg, &e); err != nil { panic(err)}if e。Table == “blog” { var blogs []Blog if err := mapstructure。Decode(e。Data, &blogs); err != nil { panic(err) } fmt。Println(blogs)}

event 的解析和前面的一樣,透過 e。Table 判斷是是否來自 blog 表的資料,如果是,使用 Blog 結構體解析。接下來透過 mapstructure 的 Decode 完成解析。

列印結果如下:

[{100001 title this is a blog 1000012 1}]

到此,似乎已經完成了所有工作。非也!

弱型別解析

不知道大家有沒有發現一個問題,那就是 Blog 結構體中的所有成員都是 string,這應該是 canal 做的事情,所有的值型別都是 string。但實際上 blog 表中的 uid 和 state 欄位其實都是 int。

理想的結構體定義應該是下面這樣。

type Blog struct { BlogId string `mapstructure:“blogId”` Title string `mapstructrue:“title”` Content string `mapstructure:“content”` Uid int32 `mapstructure:“uid”` State int32 `mapstructure:“state”`}

但是當把新的 Blog 型別代入之前的程式碼,會如下的錯誤。

panic: 2 error(s) decoding:* ‘[0]。state’ expected type ‘int32’, got unconvertible type ‘string’* ‘[0]。uid’ expected type ‘int32’, got unconvertible type ‘string’

提示型別解析失敗。其實,這種形式的 json 在其他一些軟型別語言中也會出現。

那如何解決這個問題?提兩種解決方案

使用時進行轉化,比如型別為 int 的資料,使用時可以用 strconv。Atoi 轉化。

使用 mapstructure 提供的軟型別 map 轉化 struct 的功能;

顯然,第一種方式太 low,轉化的時候還要多一步錯誤檢查。那第二種方式如何呢?

來看示例程式碼,如下:

var blogs []Blogif err := mapstructure。WeakDecode(e。Data, &blogs); err != nil { panic(err)}fmt。Println(blogs)

其實只需要把 mapstructure 的 Decode 替換成 WeakDecode 就行了,字如其意,弱解析。如此easy。

到此,才算完成!接下來的資料處理就簡單很多了。如果想學習 mapstructure 的使用,敲敲原始碼中例子應該差不多了。

總結

本文由一個問題引出主題,如何處理不確定結構的 json 資料,開頭提出了三種可行的解決方案,三種方案是逐層遞進的。最終的方式需要依賴反射實現,當然同樣的問題別人早就想到了,並開發了一個第三方包,mapstructure。最後,本文透過一個實際的案例演示了 mapstructure 的使用。