淺析golang如何處理json中的null
最近學(xué)習(xí) go 發(fā)現(xiàn)發(fā)現(xiàn)處理 json 中的 null 時(shí),會(huì)這么難受,需要專門寫一篇文章來講解一下
以下是正文
json 是一種常用的數(shù)據(jù)格式,在 go 使用 json 序列化和反序列化時(shí)比較方便的,但在使用過程中,會(huì)遇到一些問題,比如 null
由于 go 沒有聯(lián)合類型,當(dāng) json 中有個(gè)屬性為 null 時(shí),就無法直接將 null 轉(zhuǎn)換成 nil 后賦值給某個(gè)具體的類型
比如下面這個(gè)例子:
Name 定一個(gè)的是 string 類型,但在 json 中 name 的值為 null,直接轉(zhuǎn)換會(huì)報(bào)錯(cuò)
type Tag struct {
ID int `json:"id"`
Name string `json:"name"`
}
tag := Tag{
ID: 1,
Name: nil, // 這里會(huì)報(bào)錯(cuò)
}這種問題不光出現(xiàn)在 json 解析時(shí),還會(huì)出現(xiàn)在數(shù)據(jù)庫讀寫時(shí)
比如在數(shù)據(jù)庫中,某個(gè)字段的值為 NULL,在讀取時(shí),會(huì)被解析成 nil,但是 go 中的類型是不能直接賦值為 nil 的
所以在這兩種場景下該怎么解決呢?
一般有三種方法:
- 使用指針
- 自定義類型
- 使用第三方庫
使用指針
go 的指針類型是可以賦值為 nil 的,所以我們使用指針解決這個(gè)問題
我們把上面例子中的 Name 定義為 string 的指針類型,如下代碼:
type Tag struct {
ID int `json:"id"`
Name *string `json:"name"`
}
name := "uccs" // 定義一個(gè) string 類型的變量,因?yàn)椴荒馨岩粋€(gè)字面量直接賦值給指針類型
tag := Tag{
ID: 1,
Name: &name, // 將 name 的地址賦值給 Name,使用 & 地址符
}在使用時(shí),需要先判斷一下 Name 是為 nil,如果不為 nil,則使用 * 取值符取出值
// Name 是指針類型,判斷是否為 nil 時(shí)不需要使用 * 取值符
if tag.Name != nil {
// Name 是指針類型,取值時(shí)需要使用 * 取值符
if *tag.Name == "uccs" {
// ...
}
}注意事項(xiàng)
ORM 框架會(huì)實(shí)現(xiàn)一個(gè) NullString 的類型,
當(dāng)我們?cè)诙x Model 時(shí),如果某個(gè)字段可以為 NULL,則 ORM 框架會(huì)把它定義為 NullString 類型(下文講解)
給指針賦值時(shí),不能直接使用字面量,需要先定義一個(gè)變量,然后將變量的地址賦值給指針
使用指針時(shí)需要注意,這里會(huì)比較繞
在判斷是否為 nil 時(shí),不需要使用 * 取值符
在判斷是否為 uccs 時(shí),需要使用 * 取值符
當(dāng)遇到 panic: runtime error: invalid memory address or nil pointer dereference 錯(cuò)誤時(shí),說明指針為 nil
也就是說使用指針時(shí),我們最需要注意的是:在指針上取值時(shí),一定要注意它是不是為 nil
自定義類型
我們使用結(jié)構(gòu)體定義一個(gè)類型:NullString,它有兩個(gè)屬性 String 和 Valid
String 用來存儲(chǔ)字符串
Valid 用來標(biāo)識(shí) String 是否有值
- 如果
Valid為true,則String有值 - 如果
Valid為false,則String是空值""
type NullString struct {
String string
Valid bool
}當(dāng)我們定義好類型后,需要考考慮兩個(gè)問題:
- 如何解決
json解析時(shí)null的問題 - 如何向數(shù)據(jù)庫進(jìn)行讀寫
go 有個(gè)特點(diǎn),你自定義的類型有某些方法,那么在某些場景下,這些方法會(huì)被調(diào)用
比如,序列化時(shí),會(huì)調(diào)用 MarshalJSON 方法,反序列化時(shí),會(huì)調(diào)用 UnmarshalJSON 方法
你的自定義類型實(shí)現(xiàn)了這兩個(gè)方法,那么在序列化和反序列化時(shí),這兩個(gè)方法就會(huì)被調(diào)用
數(shù)據(jù)庫讀寫是實(shí)現(xiàn) Scan 和 Value 方法
所以下面就從這兩塊講起:
序列化和反序列化
我們給 NullString 類型添加兩個(gè)方法 MarshalJSON 和 UnmarshalJSON
// 序列化時(shí)
func (ns NullString) MarshalJSON() ([]byte, error) {
// 如果 Valid 為 true,則返回 String 的 json 序列化結(jié)果
if ns.Valid {
return []byte(`"` + ns.String + `"`), nil
}
// 如果 Valid 為 false,則返回 null 序列化的結(jié)果
return []byte("null"), nil
}
// 反序列化
func (ns *NullString) UnmarshalJSON(data []byte) error {
// 如果 data 為 null,則 Valid 為 false
// String 為空字符串
if string(data) == "null" {
ns.String, ns.Valid = "", false
return nil
}
// 否則,將 data 反序列化到 String 中
// 并將 Valid 設(shè)置為 true
if err := json.Unmarshal(data, &ns.String); err != nil {
return err
}
ns.Valid = true
return nil
}有了這兩個(gè)方法之后,我們就解決了 json 解析時(shí) null 的問題
是什么時(shí)候會(huì)觸發(fā)這兩個(gè)方法呢?
從 json 內(nèi)容解析填充 struct 的場景時(shí)會(huì)觸發(fā) UnmarshalJSON 的調(diào)用
- 直接調(diào)用
json.Unmarshal對(duì)json數(shù)據(jù)進(jìn)行解析時(shí) http.Request讀取json Body時(shí)- 使用
encoding/json的Decoder進(jìn)行解碼時(shí) - 對(duì)實(shí)現(xiàn)了
Unmarshaler接口的對(duì)象調(diào)用UnmarshalJSON方法時(shí)
反過來,將 struct 內(nèi)容序列化為 json 時(shí)會(huì)觸發(fā) json.Marshal 的調(diào)用
- 直接調(diào)用
json.Marshal對(duì)一個(gè)對(duì)象進(jìn)行編碼 - 使用
http.ResponseWriter的Write方法響應(yīng)json數(shù)據(jù)時(shí) - 使用
encoding/json的Encoder進(jìn)行編碼時(shí) - 對(duì)實(shí)現(xiàn)了
Marshaler接口的對(duì)象調(diào)用MarshalJSON方法時(shí)
序列化和反序列化問題解決了,那如何向數(shù)據(jù)庫進(jìn)行讀寫呢?
數(shù)據(jù)庫讀寫
我們?cè)俳o NullString 添加兩個(gè)方法 Value 和 Scan
Value方法會(huì)在寫入數(shù)據(jù)庫時(shí)被調(diào)用Scan方法會(huì)在從數(shù)據(jù)庫讀取時(shí)被調(diào)用
// Scan 方法在 數(shù)據(jù)庫讀取時(shí)被調(diào)用
func (ns *NullString) Scan(value interface{}) error {
// 如果 value 為 nil,則 Valid 為 false,String 為空字符串
if value == nil {
ns.String, ns.Valid = "", false
return nil
}
// 否則,將 value 斷言為 string 類型,斷言成功 Valid 為 true,String 為 value
ns.String, ns.Valid = value.(string)
return nil
}
// Value 方法 在寫入數(shù)據(jù)庫時(shí)被調(diào)用
func (ns NullString) Value() (driver.Value, error) {
// 如果 Valid 為 false,則返回 nil
if !ns.Valid {
return nil, nil
}
// 否則,返回 String
return ns.String, nil
}添加這兩個(gè)方法后,我們就可以向數(shù)據(jù)庫中寫入 null 了
是什么時(shí)候會(huì)觸發(fā)這兩個(gè)方法呢?
Scanner 接口的 Scan 方法會(huì)在以下情況被調(diào)用
ORM 框架如 GORM、database/sql 等查詢時(shí),掃描結(jié)果到自定義模型
Valuer 接口的 Value 方法會(huì)在以下情況被調(diào)用
ORM 框架如 GORM、database/sql 構(gòu)造寫入語句時(shí),獲取自定義模型的值
使用
將上面 Tag 的解構(gòu)體改為:
type Tag struct {
ID int `json:"id"`
Name NullString `json:"name"`
}不過這里要注意的一點(diǎn)是,在給 Name 賦值時(shí),需要使用 NullString 進(jìn)行賦值,如果下所示:
tag := Tag{
ID: 1,
Name: NullString{String: "hello", Valid: true},
}最后需要注意的是,go 中其他類型也要實(shí)現(xiàn)這樣的方法,比如 NullInt,NullBool 等,可以參照這個(gè) guregu/null 這個(gè)庫
使用第三方庫
第三方庫 guregu/null 已經(jīng)實(shí)現(xiàn)了上面的方法,我們可以直接使用
ORM 一般都實(shí)現(xiàn)了這些功能
需要注意的是有些 ORM 只實(shí)現(xiàn)了 Scanner 和 Valuer 接口,沒有實(shí)現(xiàn) MarshalJSON 和 UnmarshalJSON 接口
總結(jié)
- 使用
string只能滿足必填的情況 ORM框架一般都實(shí)現(xiàn)了Scanner和Valuer接口,但是有些ORM沒有實(shí)現(xiàn)MarshalJSON和UnmarshalJSON接口,需要自己實(shí)現(xiàn),或者使用第三方庫- 使用指針時(shí),如
*string,需要注意指針是否為nil
到此這篇關(guān)于淺析golang如何處理json中的null的文章就介紹到這了,更多相關(guān)go處理json內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Golang開發(fā)一個(gè)輕量級(jí)登錄庫/框架
幾乎每個(gè)項(xiàng)目都會(huì)有登錄,退出等用戶功能,而登錄又不單僅僅是登錄,我們要考慮很多東西。所以本文就來用Golang開發(fā)一個(gè)輕量級(jí)登錄庫/框架吧2023-05-05
golang實(shí)現(xiàn)命令行程序的使用幫助功能
這篇文章介紹了golang實(shí)現(xiàn)命令行程序使用幫助的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07

