koanf 是一个用于在 Go 应用程序中从不同来源读取不同格式配置的库。它是 spf13/viper 的替代方案,更加简洁、轻量,具有更好的抽象和可扩展性,依赖项也更少。
koanf v2 有多个模块(Providers)用于从各种来源读取配置,如文件、命令行标志、环境变量、Vault 和 S3,以及用于解析(Parsers)JSON、YAML、TOML、Hashicorp HCL 等格式。它很容易插入自定义解析器和提供者。
providers 和 parsers 中的所有外部依赖都与核心分离,可以根据需要单独安装。
安装
# 安装核心库
go get -u github.com/knadh/koanf/v2
# 安装必要的 Provider(s)
# 可用的有:file, env, posflag, basicflag, confmap, rawbytes,
# structs, fs, s3, appconfig/v2, consul/v2, etcd/v2, vault/v2, parameterstore/v2
# 例如:go get -u github.com/knadh/koanf/providers/s3
# 例如:go get -u github.com/knadh/koanf/providers/consul/v2
go get -u github.com/knadh/koanf/providers/file
# 安装必要的 Parser(s)
# 可用的有:toml, toml/v2, json, yaml, dotenv, hcl, hjson, nestedtext
# go get -u github.com/knadh/koanf/parsers/$parser
go get -u github.com/knadh/koanf/parsers/toml
查看所有内置 Providers 和 Parsers 的列表。
目录
- 概念
- 从文件读取配置
- 监视文件变化
- 从命令行读取
- 读取环境变量
- 读取原始字节
- 从 map 和结构体读取
- 解组和编组
- 合并顺序和键大小写敏感性
- 自定义 Providers 和 Parsers
- 自定义合并策略
- 可安装的 Providers 和 Parsers 列表
概念
koanf.Provider
是一个通用接口,提供配置,例如从文件、环境变量、HTTP 源或任何地方。配置可以是解析器可以解析的原始字节,也可以是可以直接加载的嵌套map[string]interface{}
。koanf.Parser
是一个通用接口,接收原始字节,解析并返回嵌套的map[string]interface{}
。例如 JSON 和 YAML 解析器。- 一旦加载到 koanf 中,配置值可以通过分隔的键路径语法进行查询。例如:
app.server.port
。可以选择任何分隔符。 - 可以从多个来源加载配置并合并到一个 koanf 实例中,例如,先从文件加载,然后用命令行标志覆盖某些值。
通过这两个接口实现,koanf 可以从任何来源获取任何格式的配置,解析它,并使其可用于应用程序。
从文件读取配置
package main
import (
"fmt"
"log"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
)
// 全局 koanf 实例。使用 "." 作为键路径分隔符。这可以是 "/" 或任何字符。
var k = koanf.New(".")
func main() {
// 加载 JSON 配置
if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
log.Fatalf("加载配置错误:%v", err)
}
// 加载 YAML 配置并合并到之前加载的配置中(因为我们可以这样做)
k.Load(file.Provider("mock/mock.yml"), yaml.Parser())
fmt.Println("父级的名称是 = ", k.String("parent1.name"))
fmt.Println("父级的 ID 是 = ", k.Int("parent1.id"))
}
监视文件变化
一些提供者暴露了 Watch()
方法,使提供者能够监视配置变化并触发回调以重新加载配置。
如果在 koanf 对象执行 Load()
时有并发的 *Get()
调用发生,这不是线程安全的。这种情况需要互斥锁定。
file, appconfig, vault, consul
提供者都有 Watch()
方法。
package main
import (
"fmt"
"log"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
)
// 全局 koanf 实例。使用 "." 作为键路径分隔符。这可以是 "/" 或任何字符。
var k = koanf.New(".")
func main() {
// 加载 JSON 配置
f := file.Provider("mock/mock.json")
if err := k.Load(f, json.Parser()); err != nil {
log.Fatalf("加载配置错误:%v", err)
}
// 加载 YAML 配置并合并到之前加载的配置中(因为我们可以这样做)
k.Load(file.Provider("mock/mock.yml"), yaml.Parser())
fmt.Println("父级的名称是 = ", k.String("parent1.name"))
fmt.Println("父级的 ID 是 = ", k.Int("parent1.id"))
// 监视文件并在变化时获得回调。回调可以执行任何操作,
// 比如重新加载配置。
// 文件提供者总是返回一个 nil `event`。
f.Watch(func(event interface{}, err error) {
if err != nil {
log.Printf("监视错误:%v", err)
return
}
// 丢弃旧配置并加载新副本。
log.Println("配置已更改。正在重新加载...")
k = koanf.New(".")
k.Load(f, json.Parser())
k.Print()
})
// 要停止文件监视器,调用:
// f.Unwatch()
// 永久阻塞(并手动对 mock/mock.json 进行更改)以
// 重新加载配置。
log.Println("永久等待中。尝试对 mock/mock.json 进行更改以实时重新加载")
<-make(chan bool)
}
从命令行读取
以下示例展示了 posflag.Provider
的使用,它是 spf13/pflag 库的包装器,这是一个高级命令行库。对于 Go 内置的 flag
包,使用 basicflag.Provider
。
package main
import (
"fmt"
"log"
"os"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/parsers/toml"
// TOML 版本 2 可在以下位置获得:
// "github.com/knadh/koanf/parsers/toml/v2"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
flag "github.com/spf13/pflag"
)
// 全局 koanf 实例。使用 "." 作为键路径分隔符。这可以是 "/" 或任何字符。
var k = koanf.New(".")
func main() {
// 使用符合 POSIX 标准的 pflag 库代替 Go 的 flag 库。
f := flag.NewFlagSet("config", flag.ContinueOnError)
f.Usage = func() {
fmt.Println(f.FlagUsages())
os.Exit(0)
}
// 要加载到 koanf 中的一个或多个配置文件的路径以及一些配置参数。
f.StringSlice("conf", []string{"mock/mock.toml"}, "一个或多个 .toml 配置文件的路径")
f.String("time", "2020-01-01", "时间字符串")
f.String("type", "xxx", "应用程序类型")
f.Parse(os.Args[1:])
```go
// 加载命令行中提供的配置文件。
cFiles, _ := f.GetStringSlice("conf")
for _, c := range cFiles {
if err := k.Load(file.Provider(c), toml.Parser()); err != nil {
log.Fatalf("加载文件出错: %v", err)
}
}
// "time"和"type"可能已从配置文件加载,但它们
// 仍可以被命令行中的值覆盖。
// 内置的posflag.Provider接收来自spf13/pflag库的flagset。
// 将Koanf实例传递给posflag有助于处理默认命令行标志值,
// 这些值在之前加载的提供程序的conf映射中可能不存在。
if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil {
log.Fatalf("加载配置出错: %v", err)
}
fmt.Println("time是 = ", k.String("time"))
}
读取环境变量
package main
import (
"fmt"
"log"
"strings"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
)
// 全局koanf实例。使用.作为键路径分隔符。这可以是/或任何其他字符。
var k = koanf.New(".")
func main() {
// 加载JSON配置。
if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
log.Fatalf("加载配置出错: %v", err)
}
// 加载环境变量并合并到已加载的配置中。
// "MYVAR"是用于过滤环境变量的前缀。
// "."是用于表示环境变量中键层次结构的分隔符。
// (可选的,或可以为nil)函数可用于转换环境变量名称,例如将其转为小写。
//
// 例如,环境变量:MYVAR_TYPE和MYVAR_PARENT1_CHILD1_NAME
// 将被合并到配置文件中的"type"和嵌套的"parent1.child1.name"键中,
// 因为我们将键转为小写,将`_`替换为`.`,并去掉MYVAR_前缀,
// 所以只剩下"parent1.child1.name"。
k.Load(env.Provider("MYVAR_", ".", func(s string) string {
return strings.Replace(strings.ToLower(
strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1)
}), nil)
fmt.Println("name是 = ", k.String("parent1.child1.name"))
}
你也可以使用env.ProviderWithValue
,通过回调函数同时修改键和值,
以返回字符串以外的类型。例如,这里用空格分隔的环境变量值被返回为字符串切片或数组。
例如:MYVAR_slice=a b c
变成slice: [a, b, c]
。
k.Load(env.ProviderWithValue("MYVAR_", ".", func(s string, v string) (string, interface{}) {
// 去掉MYVAR_前缀并转为小写,同时将键中的_字符替换为.(koanf分隔符)。
key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "MYVAR_")), "_", ".", -1)
// 如果值中包含空格,则按空格将值分割成切片。
if strings.Contains(v, " ") {
return key, strings.Split(v, " ")
}
// 否则,返回普通字符串。
return key, v
}), nil)
从S3桶读取
// 从s3加载JSON配置。
if err := k.Load(s3.Provider(s3.Config{
AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"),
SecretKey: os.Getenv("AWS_S3_SECRET_KEY"),
Region: os.Getenv("AWS_S3_REGION"),
Bucket: os.Getenv("AWS_S3_BUCKET"),
ObjectKey: "dir/config.json",
}), json.Parser()); err != nil {
log.Fatalf("加载配置出错: %v", err)
}
读取原始字节
内置的rawbytes
Provider可用于从源(如数据库或HTTP调用)读取任意字节。
package main
import (
"fmt"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/rawbytes"
)
// 全局koanf实例。使用.作为键路径分隔符。这可以是/或任何其他字符。
var k = koanf.New(".")
func main() {
b := []byte(`{"type": "rawbytes", "parent1": {"child1": {"type": "rawbytes"}}}`)
k.Load(rawbytes.Provider(b), json.Parser())
fmt.Println("type是 = ", k.String("parent1.child1.type"))
}
解析和序列化
Parser
可用于根据字段标签将Koanf实例中的值解析并扫描到结构体中,
也可用于将Koanf实例序列化回字节,例如,重新转为JSON或YAML,以便写回文件。
package main
import (
"fmt"
"log"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/file"
)
// 全局koanf实例。使用.作为键路径分隔符。这可以是/或任何其他字符。
var (
k = koanf.New(".")
parser = json.Parser()
)
func main() {
// 加载JSON配置。
if err := k.Load(file.Provider("mock/mock.json"), parser); err != nil {
log.Fatalf("加载配置出错: %v", err)
}
// 用于解析嵌套配置的结构体。
type childStruct struct {
Name string `koanf:"name"`
Type string `koanf:"type"`
Empty map[string]string `koanf:"empty"`
GrandChild struct {
Ids []int `koanf:"ids"`
On bool `koanf:"on"`
} `koanf:"grandchild1"`
}
var out childStruct
// 快速解析。
k.Unmarshal("parent1.child1", &out)
fmt.Println(out)
// 使用高级配置解析。
out = childStruct{}
k.UnmarshalWithConf("parent1.child1", &out, koanf.UnmarshalConf{Tag: "koanf"})
fmt.Println(out)
// 将实例重新序列化为JSON。
// parser实例可以是任何类型,例如:json.Parser()、yaml.Parser()等。
b, _ := k.Marshal(parser)
fmt.Println(string(b))
}
使用平面路径解析
有时需要将各种嵌套结构中的一组键解析到一个平面目标结构中。
这可以通过UnmarshalConf.FlatPaths
标志实现。
package main
import (
"fmt"
"log"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/file"
)
// 全局koanf实例。使用.作为键路径分隔符。这可以是/或任何其他字符。
var k = koanf.New(".")
func main() {
// 加载JSON配置。
if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
log.Fatalf("加载配置出错: %v", err)
}
type rootFlat struct {
Type string `koanf:"type"`
Empty map[string]string `koanf:"empty"`
Parent1Name string `koanf:"parent1.name"`
Parent1ID int `koanf:"parent1.id"`
Parent1Child1Name string `koanf:"parent1.child1.name"`
Parent1Child1Type string `koanf:"parent1.child1.type"`
Parent1Child1Empty map[string]string `koanf:"parent1.child1.empty"`
Parent1Child1Grandchild1IDs []int `koanf:"parent1.child1.grandchild1.ids"`
Parent1Child1Grandchild1On bool `koanf:"parent1.child1.grandchild1.on"`
}
// 使用FlatPaths: True解析整个根。
var o1 rootFlat
k.UnmarshalWithConf("", &o1, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true})
fmt.Println(o1)
// 解析"parent1"的子结构。
type subFlat struct {
Name string koanf:"name"
ID int koanf:"id"
Child1Name string koanf:"child1.name"
Child1Type string koanf:"child1.type"
Child1Empty map[string]string koanf:"child1.empty"
Child1Grandchild1IDs []int koanf:"child1.grandchild1.ids"
Child1Grandchild1On bool koanf:"child1.grandchild1.on"
}
var o2 subFlat k.UnmarshalWithConf("parent1", &o2, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true}) fmt.Println(o2) }
#### 从嵌套映射中读取
内置的`confmap`提供程序接受一个`map[string]interface{}`,可以加载到koanf实例中。
```go
package main
import (
"fmt"
"log"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/yaml"
)
// 全局koanf实例。使用"."作为键路径分隔符。这可以是"/"或任何字符。
var k = koanf.New(".")
func main() {
// 使用confmap提供程序加载默认值。
// 我们提供一个使用"."分隔符的平面映射。
// 通过将分隔符设置为空字符串"",可以加载嵌套映射。
k.Load(confmap.Provider(map[string]interface{}{
"parent1.name": "Default Name",
"parent3.name": "New name here",
}, "."), nil)
// 在默认值之上加载JSON配置。
if err := k.Load(file.Provider("mock/mock.json"), json.Parser()); err != nil {
log.Fatalf("加载配置时出错: %v", err)
}
// 加载YAML配置并合并到先前加载的配置中(因为我们可以)。
k.Load(file.Provider("mock/mock.yml"), yaml.Parser())
fmt.Println("parent的名字是 = ", k.String("parent1.name"))
fmt.Println("parent的ID是 = ", k.Int("parent1.id"))
}
从结构体中读取
内置的structs
提供程序可用于从结构体中读取数据并加载到koanf实例中。
package main
import (
"fmt"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/providers/structs"
)
// 全局koanf实例。使用"."作为键路径分隔符。这可以是"/"或任何字符。
var k = koanf.New(".")
type parentStruct struct {
Name string `koanf:"name"`
ID int `koanf:"id"`
Child1 childStruct `koanf:"child1"`
}
type childStruct struct {
Name string `koanf:"name"`
Type string `koanf:"type"`
Empty map[string]string `koanf:"empty"`
Grandchild1 grandchildStruct `koanf:"grandchild1"`
}
type grandchildStruct struct {
Ids []int `koanf:"ids"`
On bool `koanf:"on"`
}
type sampleStruct struct {
Type string `koanf:"type"`
Empty map[string]string `koanf:"empty"`
Parent1 parentStruct `koanf:"parent1"`
}
func main() {
// 使用structs提供程序加载默认值。
// 我们向提供程序提供一个结构体以及结构体标签`koanf`。
k.Load(structs.Provider(sampleStruct{
Type: "json",
Empty: make(map[string]string),
Parent1: parentStruct{
Name: "parent1",
ID: 1234,
Child1: childStruct{
Name: "child1",
Type: "json",
Empty: make(map[string]string),
Grandchild1: grandchildStruct{
Ids: []int{1, 2, 3},
On: true,
},
},
},
}, "koanf"), nil)
fmt.Printf("名字是 = `%s`\n", k.String("parent1.child1.name"))
}
合并行为
默认行为
当你这样创建Koanf时的默认行为是:koanf.New(delim)
,最新加载的配置将与之前的配置合并。
例如:
first.yml
key: [1,2,3]
second.yml
key: 'string'
当加载second.yml
时,它将覆盖first.yml
的类型。
如果不希望这种行为,你可以进行"严格"合并。在相同的场景下,Load
将返回一个错误。
package main
import (
"errors"
"log"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/maps"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
)
var conf = koanf.Conf{
Delim: ".",
StrictMerge: true,
}
var k = koanf.NewWithConf(conf)
func main() {
yamlPath := "mock/mock.yml"
if err := k.Load(file.Provider(yamlPath), yaml.Parser()); err != nil {
log.Fatalf("加载配置时出错: %v", err)
}
jsonPath := "mock/mock.json"
if err := k.Load(file.Provider(jsonPath), json.Parser()); err != nil {
log.Fatalf("加载配置时出错: %v", err)
}
}
注意: 当合并不同的扩展名时,每个解析器可能会以不同方式处理其类型,
这意味着即使你加载相同的类型,使用StrictMerge: true
也可能会失败。
例如:合并JSON和YAML很可能会失败,因为JSON将整数视为float64,而YAML将它们视为整数。
合并顺序和键的大小写敏感性
- koanf中的配置键是大小写敏感的。例如,
app.server.port
和APP.SERVER.port
不是相同的。 - koanf不对从各种提供程序加载配置的顺序施加任何限制。每次连续的
Load()
或Merge()
都会将新配置合并到现有配置中。也就是说,可以先加载环境变量,然后在其上加载文件,然后在其上加载命令行变量,或任何这样的顺序。
自定义提供程序和解析器
提供程序返回一个嵌套的map[string]interface{}
配置,可以直接使用koanf.Load()
加载到koanf中,或者它可以返回原始字节,可以使用解析器进行解析(同样使用koanf.Load()
加载)。编写提供程序和解析器很简单。请查看providers和parsers目录中的内置实现。
自定义合并策略
默认情况下,使用Load()
合并两个配置源时,koanf会递归合并嵌套映射(map[string]interface{}
)的键,
而静态值(切片、字符串等)会被覆盖。可以通过提供带有WithMergeFunc
选项的自定义合并函数来更改此行为。
package main
import (
"errors"
"log"
"github.com/knadh/koanf/v2"
"github.com/knadh/koanf/maps"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
)
var conf = koanf.Conf{
Delim: ".",
StrictMerge: true,
}
var k = koanf.NewWithConf(conf)
func main() {
yamlPath := "mock/mock.yml"
if err := k.Load(file.Provider(yamlPath), yaml.Parser()); err != nil {
log.Fatalf("加载配置时出错: %v", err)
}
jsonPath := "mock/mock.json"
if err := k.Load(file.Provider(jsonPath), json.Parser(), koanf.WithMergeFunc(func(src, dest map[string]interface{}) error {
// 你的自定义逻辑,将值从src复制到dst
return nil
})); err != nil {
log.Fatalf("加载配置时出错: %v", err)
}
}
API
查看https://pkg.go.dev/github.com/knadh/koanf/v2#section-documentation 获取所有可用方法的完整API文档
内置提供程序
使用go get -u github.com/knadh/koanf/providers/$provider
安装
包名 | 提供者 | 描述 |
---|---|---|
file | file.Provider(filepath string) | 读取文件并返回待解析的原始字节。 |
fs | fs.Provider(f fs.FS, filepath string) | (实验性) 从fs.FS读取文件并返回待解析的原始字节。此提供者需要go v1.16 或更高版本。 |
basicflag | basicflag.Provider(f *flag.FlagSet, delim string) | 接受标准库的flag.FlagSet |
posflag | posflag.Provider(f *pflag.FlagSet, delim string) | 接受spf13/pflag.FlagSet (支持多种类型的高级POSIX兼容标志),并基于分隔符提供嵌套的配置映射。 |
env | env.Provider(prefix, delim string, f func(s string) string) | 接受一个可选的前缀用于过滤环境变量,一个可选的函数用于转换环境变量(接受并返回字符串),并基于分隔符返回嵌套的配置映射。 |
confmap | confmap.Provider(mp map[string]interface{}, delim string) | 接受预先制作的map[string]interface{} 配置映射。如果提供了分隔符,则假定键是扁平化的,因此使用分隔符进行解扁平化。 |
structs | structs.Provider(s interface{}, tag string) | 接受一个结构体和结构体标签。 |
s3 | s3.Provider(s3.S3Config{}) | 接受一个s3配置结构体。 |
rawbytes | rawbytes.Provider(b []byte) | 接受一个原始[]byte 切片,用koanf.Parser进行解析 |
vault/v2 | vault.Provider(vault.Config{}) | Hashicorp Vault提供者 |
appconfig/v2 | vault.AppConfig(appconfig.Config{}) | AWS AppConfig提供者 |
etcd/v2 | etcd.Provider(etcd.Config{}) | CNCF etcd提供者 |
consul/v2 | consul.Provider(consul.Config{}) | Hashicorp Consul提供者 |
parameterstore/v2 | parameterstore.Provider(parameterstore.Config{}) | AWS Systems Manager Parameter Store提供者 |
内置解析器
通过 go get -u github.com/knadh/koanf/parsers/$parser
安装
包名 | 解析器 | 描述 |
---|---|---|
json | json.Parser() | 将JSON字节解析为嵌套映射 |
yaml | yaml.Parser() | 将YAML字节解析为嵌套映射 |
toml | toml.Parser() | 将TOML字节解析为嵌套映射 |
toml/v2 | toml.Parser() | 将TOML字节解析为嵌套映射(使用go-toml v2) |
dotenv | dotenv.Parser() | 将DotEnv字节解析为扁平映射 |
hcl | hcl.Parser(flattenSlices bool) | 将Hashicorp HCL字节解析为嵌套映射。建议将flattenSlices 设置为true。了解更多。 |
nestedtext | nestedtext.Parser() | 将NestedText字节解析为扁平映射 |
hjson | hjson.Parser() | 将HJSON字节解析为嵌套映射 |
第三方提供者
包名 | 提供者 | 描述 |
---|---|---|
github.com/defensestation/koanf/providers/secretsmanager | vault.SecretsMananger(secretsmanager.Config{}, f func(s string) string) | AWS Secrets Manager提供者,从存储中接受映射或字符串作为值 |
github.com/defensestation/koanf/providers/parameterstore | vault.ParameterStore(parameterstore.Config{}, f func(s string) string) | AWS ParameterStore提供者,一个可选的函数用于转换环境变量(接受并返回字符串) |
viper的替代方案
koanf是流行的spf13/viper的轻量级替代方案。它是为了解决使用viper时遇到的一些根本性问题而编写的。
- viper通过强制将键转为小写违反了JSON、YAML、TOML、HCL语言规范。
- 显著增加了构建大小。
- 将配置解析与文件扩展名紧密耦合。
- 语义和抽象不佳。命令行、环境变量、文件等以及各种解析器都硬编码在核心中。没有可扩展的基本元素。
- 将大量第三方依赖引入核心包。例如,即使不使用YAML或标志,由于耦合,这些依赖仍会被引入。
- 强加任意的排序约定(例如:标志 -> 环境变量 -> 配置等)
Get()
返回切片和映射的引用。外部进行的修改会改变配置映射中的底层值。- 做一些非惯用的事情,如在扁平映射上放弃O(1)。
- Viper将包含空映射的键(例如:
my_key: {}
)视为未设置(即:IsSet("my_key") == false
)。 - 存在大量未解决的问题。