淺析golang如何處理json中的null
最近學(xué)習(xí) go
發(fā)現(xiàn)發(fā)現(xiàn)處理 json
中的 null
時(shí),會(huì)這么難受,需要專門寫一篇文章來(lái)講解一下
以下是正文
json
是一種常用的數(shù)據(jù)格式,在 go
使用 json
序列化和反序列化時(shí)比較方便的,但在使用過(guò)程中,會(huì)遇到一些問(wèn)題,比如 null
由于 go
沒(méi)有聯(lián)合類型,當(dāng) json
中有個(gè)屬性為 null
時(shí),就無(wú)法直接將 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ò) }
這種問(wèn)題不光出現(xiàn)在 json
解析時(shí),還會(huì)出現(xiàn)在數(shù)據(jù)庫(kù)讀寫時(shí)
比如在數(shù)據(jù)庫(kù)中,某個(gè)字段的值為 NULL
,在讀取時(shí),會(huì)被解析成 nil
,但是 go
中的類型是不能直接賦值為 nil
的
所以在這兩種場(chǎng)景下該怎么解決呢?
一般有三種方法:
- 使用指針
- 自定義類型
- 使用第三方庫(kù)
使用指針
go
的指針類型是可以賦值為 nil
的,所以我們使用指針解決這個(gè)問(wèn)題
我們把上面例子中的 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í),說(shuō)明指針為 nil
也就是說(shuō)使用指針時(shí),我們最需要注意的是:在指針上取值時(shí),一定要注意它是不是為 nil
自定義類型
我們使用結(jié)構(gòu)體定義一個(gè)類型:NullString
,它有兩個(gè)屬性 String
和 Valid
String
用來(lái)存儲(chǔ)字符串
Valid
用來(lái)標(biāo)識(shí) String
是否有值
- 如果
Valid
為true
,則String
有值 - 如果
Valid
為false
,則String
是空值""
type NullString struct { String string Valid bool }
當(dāng)我們定義好類型后,需要考考慮兩個(gè)問(wèn)題:
- 如何解決
json
解析時(shí)null
的問(wèn)題 - 如何向數(shù)據(jù)庫(kù)進(jìn)行讀寫
go
有個(gè)特點(diǎn),你自定義的類型有某些方法,那么在某些場(chǎng)景下,這些方法會(huì)被調(diào)用
比如,序列化時(shí),會(huì)調(diào)用 MarshalJSON
方法,反序列化時(shí),會(huì)調(diào)用 UnmarshalJSON
方法
你的自定義類型實(shí)現(xiàn)了這兩個(gè)方法,那么在序列化和反序列化時(shí),這兩個(gè)方法就會(huì)被調(diào)用
數(shù)據(jù)庫(kù)讀寫是實(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
的問(wèn)題
是什么時(shí)候會(huì)觸發(fā)這兩個(gè)方法呢?
從 json
內(nèi)容解析填充 struct
的場(chǎng)景時(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í)
反過(guò)來(lái),將 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í)
序列化和反序列化問(wèn)題解決了,那如何向數(shù)據(jù)庫(kù)進(jìn)行讀寫呢?
數(shù)據(jù)庫(kù)讀寫
我們?cè)俳o NullString
添加兩個(gè)方法 Value
和 Scan
Value
方法會(huì)在寫入數(shù)據(jù)庫(kù)時(shí)被調(diào)用Scan
方法會(huì)在從數(shù)據(jù)庫(kù)讀取時(shí)被調(diào)用
// Scan 方法在 數(shù)據(jù)庫(kù)讀取時(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ù)庫(kù)時(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ù)庫(kù)中寫入 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)造寫入語(yǔ)句時(shí),獲取自定義模型的值
使用
將上面 Tag
的解構(gòu)體改為:
type Tag struct { ID int `json:"id"` Name NullString `json:"name"` }
不過(guò)這里要注意的一點(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è)庫(kù)
使用第三方庫(kù)
第三方庫(kù) guregu/null 已經(jīng)實(shí)現(xiàn)了上面的方法,我們可以直接使用
ORM
一般都實(shí)現(xiàn)了這些功能
需要注意的是有些 ORM
只實(shí)現(xiàn)了 Scanner
和 Valuer
接口,沒(méi)有實(shí)現(xiàn) MarshalJSON
和 UnmarshalJSON
接口
總結(jié)
- 使用
string
只能滿足必填的情況 ORM
框架一般都實(shí)現(xiàn)了Scanner
和Valuer
接口,但是有些ORM
沒(méi)有實(shí)現(xiàn)MarshalJSON
和UnmarshalJSON
接口,需要自己實(shí)現(xiàn),或者使用第三方庫(kù)- 使用指針時(shí),如
*string
,需要注意指針是否為nil
到此這篇關(guān)于淺析golang如何處理json中的null的文章就介紹到這了,更多相關(guān)go處理json內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Golang開(kāi)發(fā)一個(gè)輕量級(jí)登錄庫(kù)/框架
幾乎每個(gè)項(xiàng)目都會(huì)有登錄,退出等用戶功能,而登錄又不單僅僅是登錄,我們要考慮很多東西。所以本文就來(lái)用Golang開(kāi)發(fā)一個(gè)輕量級(jí)登錄庫(kù)/框架吧2023-05-05golang實(shí)現(xiàn)命令行程序的使用幫助功能
這篇文章介紹了golang實(shí)現(xiàn)命令行程序使用幫助的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07