今天咱们介绍一个合并结构体字段的库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】,共同窗习,一块儿进步~