Go 每日一庫之 mergo

熱烈歡迎你,相識是一種緣分,Echa 哥為了你的到來特意準備了一份驚喜,go學習資料《 》


Go 每日一庫之 mergo


簡介

今天我們介紹一個合併結構體字段的庫mergo。mergo可以在相同的結構體或map之間賦值,可以將結構體的字段賦值到map中,可以將map的值賦值給結構體的字段。感謝@thinkgos推薦。

快速使用

先安裝:

<code>$ go get github.com/imdario/mergo
複製代碼/<code>

後使用:

<code>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)
}
複製代碼/<code>

使用非常簡單。mergo提供了兩組接口(其實就是兩個,*WithOverwrite已經廢棄了,可使用WithOverride選項代替):

  • Merge:合併兩個相同類型的結構或map;
  • Map:在結構和map之間賦值。

參數 1 是目標對象,參數 2 是源對象,這兩個函數的功能就是將源對象中的字段複製到目標對象的對應字段上。

高級選項

如果僅僅只是複製結構體,為啥不直接寫redisConfig = defaultConfig呢?mergo提供了很多選項。

覆蓋

默認情況下,如果目標對象的字段已經設置了,那麼Merge/Map不會用源對象中的字段替換它。我們在上面程序的var config redisConfig定義下添加一行:

<code>config.DB = 2
複製代碼/<code>

再看看運行結果,發現輸出的db是 2,而非 1。

可以通過選項來改變這個行為,調用Merge/Map時,傳入WithOverride參數,那麼目標對象中已經設置的字段也會被覆蓋:

<code>if err := mergo.Merge(&config, defaultConfig, mergo.WithOverride); err != nil {
log.Fatal(err)
}
複製代碼/<code>

只需要修改這一行調用。結果輸出db是 1,覆蓋了!

這裡用到了 Go 中的選項模式。在參數比較多,且大部分有默認值的情況下,我們可以在函數最後添加一個可變的選項參數,通過傳入選項來改變函數的行為,不傳入的選項就使用默認值。選項模式在 Go 語言中使用非常廣泛,能大大提高代碼的可擴展性,使用可變參數也能使函數更易用。mergo中的選項都是這種形式。想要深入瞭解一下?看這裡dave.cheney.net/2014/10/17/…。

mergo老的接口MergeWithOverride和MapWithOverride都使用選項模式重構了。

切片

如果某個字段是一個切片,不覆蓋就保留目標對象的值,或者用源對象的值覆蓋都不合適。我們可能想將源對象中切片的值對添加到目標對象的字段中,這時可以使用WithAppendSlice選項。

<code>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)
}
複製代碼/<code>

我們將DB字段改為[]int類型的DBs,使用WithAppendSliec選項,最後輸出的DBs為[2 3 1]。

空值覆蓋

默認情況下,如果源對象中的字段為空值(數組、切片長度為 0 ,指針為nil,數字為 0,字符串為""等),即使我們使用了WithOverride選項也是不會覆蓋的。下面兩個選項就是強制這種情況下也覆蓋:

  • WithOverrideEmptySlice:源對象的空切片覆蓋目標對象的對應字段;
  • WithOverwriteWithEmptyValue:源對象中的空值覆蓋目標對象的對應字段,其實這個對切片也有效。

文檔中這兩個選項的介紹比較混亂,我通過看源碼和自己試驗下來發現:

  • 這兩個選項都必須和WithOverride一起使用;
  • WithOverwriteWithEmptyValue這個選項也可以處理切片類型的值。

看下面代碼:

<code>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)

}
複製代碼/<code>

最終會輸出空的DBs。

類型檢查

這個主要用在map之間的切片字段的賦值,因為使用mergo在兩個結構體之間賦值必須保證兩個結構體類型相同,沒有類型檢查的必要。因為map類型為map[string]interface{},所以默認情況下,map切片類型不一致也是可以賦值的:

<code>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)
}
複製代碼/<code>

如果添加mergo.WithTypeCheck選項,則切片類型不一致會拋出錯誤:

<code>if err := mergo.Map(&m1, &m2, mergo.WithOverride, mergo.WithTypeCheck); err != nil {
log.Fatal(err)
}
複製代碼/<code>

輸出:

<code>cannot override two slices with different type ([]int, []uint32)
exit status 1
複製代碼/<code>

注意事項

  1. mergo不會賦值非導出字段;
  2. map中對應的鍵名首字母會轉為小寫;
  3. mergo可嵌套賦值,我們演示的只有一層結構。

總結

mergo其實在很多知名項目中都有應用,如moby/kubernetes等。本文介紹了mergo的基本用法,感興趣可以去 GitHub 上深入學習。關於選項模式,這裡多說一句,我在實際項目中多次應用,能極大地提高可擴展性,方便今後添加新的功能。


分享到:


相關文章: