今天咱們介紹一個合併結構體字段的庫mergo
。mergo
能夠在相同的結構體或map
之間賦值,能夠將結構體的字段賦值到map
中,能夠將map
的值賦值給結構體的字段。感謝@thinkgos推薦。git
先安裝:github
$ go get github.com/imdario/mergo
後使用:golang
package main import ( "fmt" "log" "github.com/imdario/mergo" ) type redisConfig struct { Address string Port int DB int } var defaultConfig = redisConfig{ Address: "127.0.0.1", Port: 6381, DB: 1, } func main() { var config redisConfig if err := mergo.Merge(&config, defaultConfig); err != nil { log.Fatal(err) } fmt.Println("redis address: ", config.Address) fmt.Println("redis port: ", config.Port) fmt.Println("redis db: ", config.DB) var m = make(map[string]interface{}) if err := mergo.Map(&m, defaultConfig); err != nil { log.Fatal(err) } fmt.Println(m) }
使用很是簡單。mergo
提供了兩組接口(其實就是兩個,*WithOverwrite
已經廢棄了,可以使用WithOverride
選項代替):redis
Merge
:合併兩個相同類型的結構或map
;Map
:在結構和map
之間賦值。參數 1 是目標對象,參數 2 是源對象,這兩個函數的功能就是將源對象中的字段複製到目標對象的對應字段上。api
若是僅僅只是複製結構體,爲啥不直接寫redisConfig = defaultConfig
呢?mergo
提供了不少選項。數組
默認狀況下,若是目標對象的字段已經設置了,那麼Merge/Map
不會用源對象中的字段替換它。咱們在上面程序的var config redisConfig
定義下添加一行:微信
config.DB = 2
再看看運行結果,發現輸出的db
是 2,而非 1。ide
能夠經過選項來改變這個行爲,調用Merge/Map
時,傳入WithOverride
參數,那麼目標對象中已經設置的字段也會被覆蓋:函數
if err := mergo.Merge(&config, defaultConfig, mergo.WithOverride); err != nil { log.Fatal(err) }
只須要修改這一行調用。結果輸出db
是 1,覆蓋了!學習
這裏用到了 Go 中的選項模式。在參數比較多,且大部分有默認值的狀況下,咱們能夠在函數最後添加一個可變的選項參數,經過傳入選項來改變函數的行爲,不傳入的選項就使用默認值。選項模式在 Go 語言中使用很是普遍,能大大提升代碼的可擴展性,使用可變參數也能使函數更易用。mergo
中的選項都是這種形式。想要深刻了解一下?看這裏https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis。
mergo
老的接口MergeWithOverride
和MapWithOverride
都使用選項模式重構了。
若是某個字段是一個切片,不覆蓋就保留目標對象的值,或者用源對象的值覆蓋都不合適。咱們可能想將源對象中切片的值對添加到目標對象的字段中,這時可使用WithAppendSlice
選項。
package main import ( "fmt" "log" "github.com/imdario/mergo" ) type redisConfig struct { Address string Port int DBs []int } var defaultConfig = redisConfig{ Address: "127.0.0.1", Port: 6381, DBs: []int{1}, } func main() { var config redisConfig config.DBs = []int{2, 3} if err := mergo.Merge(&config, defaultConfig, mergo.WithAppendSlice); err != nil { log.Fatal(err) } fmt.Println("redis address: ", config.Address) fmt.Println("redis port: ", config.Port) fmt.Println("redis dbs: ", config.DBs) }
咱們將DB
字段改成[]int
類型的DBs
,使用WithAppendSliec
選項,最後輸出的DBs
爲[2 3 1]
。
默認狀況下,若是源對象中的字段爲空值(數組、切片長度爲 0 ,指針爲nil
,數字爲 0,字符串爲""等),即便咱們使用了WithOverride
選項也是不會覆蓋的。下面兩個選項就是強制這種狀況下也覆蓋:
WithOverrideEmptySlice
:源對象的空切片覆蓋目標對象的對應字段;WithOverwriteWithEmptyValue
:源對象中的空值覆蓋目標對象的對應字段,其實這個對切片也有效。文檔中這兩個選項的介紹比較混亂,我經過看源碼和本身試驗下來發現:
WithOverride
一塊兒使用;WithOverwriteWithEmptyValue
這個選項也能夠處理切片類型的值。看下面代碼:
type redisConfig struct { Address string Port int DBs []int } var defaultConfig = redisConfig{ Address: "127.0.0.1", Port: 6381, } func main() { var config redisConfig config.DBs = []int{2, 3} if err := mergo.Merge(&config, defaultConfig, mergo.WithOverride, mergo.WithOverrideEmptySlice); err != nil { log.Fatal(err) } fmt.Println("redis address: ", config.Address) fmt.Println("redis port: ", config.Port) fmt.Println("redis dbs: ", config.DBs) }
最終會輸出空的DBs
。
這個主要用在map
之間的切片字段的賦值,由於使用mergo
在兩個結構體之間賦值必須保證兩個結構體類型相同,沒有類型檢查的必要。由於map
類型爲map[string]interface{}
,因此默認狀況下,map
切片類型不一致也是能夠賦值的:
func main() { m1 := make(map[string]interface{}) m1["dbs"] = []uint32{2, 3} m2 := make(map[string]interface{}) m2["dbs"] = []int{1} if err := mergo.Map(&m1, &m2, mergo.WithOverride); err != nil { log.Fatal(err) } fmt.Println(m1) }
若是添加mergo.WithTypeCheck
選項,則切片類型不一致會拋出錯誤:
if err := mergo.Map(&m1, &m2, mergo.WithOverride, mergo.WithTypeCheck); err != nil { log.Fatal(err) }
輸出:
cannot override two slices with different type ([]int, []uint32) exit status 1
mergo
不會賦值非導出字段;map
中對應的鍵名首字母會轉爲小寫;mergo
可嵌套賦值,咱們演示的只有一層結構。mergo
其實在不少知名項目中都有應用,如moby/kubernetes
等。本文介紹了mergo
的基本用法,感興趣能夠去 GitHub 上深刻學習。關於選項模式,這裏多說一句,我在實際項目中屢次應用,能極大地提升可擴展性,方便從此添加新的功能。
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~