{"id":18643362,"url":"https://github.com/hedzr/store","last_synced_at":"2026-02-15T04:01:55.033Z","repository":{"id":226687585,"uuid":"769395307","full_name":"hedzr/store","owner":"hedzr","description":"extensible, high-performance configuration management library, optimized for hierarchical data","archived":false,"fork":false,"pushed_at":"2025-12-13T00:19:06.000Z","size":1611,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-12-14T15:14:14.535Z","etag":null,"topics":["config","config-loader","config-management","configuration","configuration-management","consul-client","etcd-client","golang","golang-library","toml","trie-structure","trie-tree","yaml"],"latest_commit_sha":null,"homepage":"https://docs.hedzr.com/docs/store/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hedzr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-03-09T00:51:04.000Z","updated_at":"2025-12-13T00:18:23.000Z","dependencies_parsed_at":"2024-03-17T12:22:53.387Z","dependency_job_id":"4cdeb541-cf38-4cf0-826f-32cfaadeda72","html_url":"https://github.com/hedzr/store","commit_stats":{"total_commits":332,"total_committers":3,"mean_commits":"110.66666666666667","dds":0.009036144578313254,"last_synced_commit":"2ed6d007983acb8ad1c5da5865081b4276f97114"},"previous_names":["hedzr/store"],"tags_count":1456,"template":false,"template_full_name":null,"purl":"pkg:github/hedzr/store","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Fstore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Fstore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Fstore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Fstore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hedzr","download_url":"https://codeload.github.com/hedzr/store/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Fstore/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29468393,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-15T01:01:38.065Z","status":"online","status_checked_at":"2026-02-15T02:00:07.449Z","response_time":118,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["config","config-loader","config-management","configuration","configuration-management","consul-client","etcd-client","golang","golang-library","toml","trie-structure","trie-tree","yaml"],"created_at":"2024-11-07T06:06:35.273Z","updated_at":"2026-02-15T04:01:55.026Z","avatar_url":"https://github.com/hedzr.png","language":"Go","funding_links":[],"categories":["Configuration","配置"],"sub_categories":["Standard CLI","标准CLI"],"readme":"# Store\n\n![nightly-build](https://github.com/hedzr/store/workflows/nightly-build/badge.svg)\n![release-build](https://github.com/hedzr/store/workflows/release-build/badge.svg)\n![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/hedzr/store)\n[![GitHub Release](https://img.shields.io/github/v/release/hedzr/store)](https://github.com/hedzr/store/releases)\u003c!--\n[![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/hedzr/store.svg?label=release)](https://github.com/hedzr/store/releases) --\u003e\n[![Go Report Card](https://goreportcard.com/badge/github.com/hedzr/store)](https://goreportcard.com/report/github.com/hedzr/store)\n[![Coverage Status](https://coveralls.io/repos/github/hedzr/store/badge.svg?branch=master\u0026.9)](https://coveralls.io/github/hedzr/store?branch=master)\n[![Go Dev](https://img.shields.io/badge/go-dev-green)](https://pkg.go.dev/github.com/hedzr/store)\n[![deps.dev](https://img.shields.io/badge/deps-dev-green)](https://deps.dev/go/github.com%2Fhedzr%2Fstore)\n[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#configuration)\n\n`hedzr/store` provides an extensible, high-performance configuration management library. It is optimized for accessing hierarchical data.\n\nThe special is `put any data and extract typed it`. Which means, the `store` will try to convert the source data within underlying.\n\nAnother feature is the `store` traces config items' modification states. So you can extract the changed subset. See also [Modified State](#modified-state).\n\nThe `store` is designed to provide the basic skeleton for [hedzr/cmdr v2 (RC1 released)](https://github.com/hedzr/cmdr). It also could be used as a standalone config manager. We split the original config (called as option-store at cmdr v1) and rewrote it. In the refactoring phrase, some excellent config-mgmt/centers get many tips to us, such as [koanf](https://github.com/knadh/koanf) and [viper](https://github.com/spf13/viper). Many respects.\n\nThe `store` accesses tree data with a dotted key path, that means you may point to a specified tree node and access it, modify it, monitor it or remove it. You can use a different delimiter char like `/` or `\\`.\n\n```go\nconf := store.New()\nconf.Set(\"app.debug\", false)\nconf.Set(\"app.verbose\", true)\nconf.Set(\"app.dump\", 3)\nconf.Set(\"app.logging.file\", \"/tmp/1.log\")\nconf.Set(\"app.server.start\", 5)\n\nss := conf.WithPrefix(\"app.logging\")\nss.Set(\"rotate\", 6)\nss.Set(\"words\", []any{\"a\", 1, false})\nss.Set(\"keys\", map[any]any{\"a\": 3.13, 1.73: \"zz\", false: true})\n\n// Tag, Comment \u0026 Description\nconf.Set(\"app.bool\", \"[on,off,   true]\")\nconf.SetComment(\"app.bool\", \"desc: a bool slice\", \"cmmt: remarks here\")\nconf.SetTag(\"app.bool\", []any{\"on\", \"off\", true})\n// println(conf.MustGetTag(\"app.bool\"))\n// println(conf.MustGetComment(\"app.bool\"))\n\nradix.StatesEnvSetColorMode(true) // to disable ansi escape sequences in dump output\n_, _ = fmt.Println(conf.Dump())   // inspect it\n\nconf.GetEx(\"app.bool\", func(node radix.Node[any], data any, branch bool, kvpair radix.KVPair) {\n  println(node.Comment())\n})\nconf.Update(\"app.bool\", func(node radix.Node[any], old any) {\n  node.SetComment(\"a bool slice\", \"remarks here\")\n})\n\n// TTL to clear the node data to zero\nconf.SetTTL(\"app.bool\", 3*time.Second, func(_ *radix.TTL[any], nd radix.Node[any]) {\n  log.Printf(\"%q (%v) cleared\", \"app.bool\", nd.Data())\n})  // since v1.2.5\ndefer conf.Close()  // when you used SetTTL, the Close() is must be had.\n\n// Set/create a node at once by SetEx()\nconf.SetEx(\"app.logging.auto-stop\", true,\n  func(path string, oldData any, node radix.Node[any], trie radix.Trie[any]) {\n    trie.SetTTL(path, 30*time.Minute,\n      func(s *radix.TTL[any], node radix.Node[any]) {\n          trie.Remove(node.Key()) // erase the key with the node\n      })\n    // Or:\n    trie.SetTTLFast(node, 39*time.Second, nil)\n    // Or:\n    node.SetTTL(39*time.Second, trie, nil)\n  })\n```\n\nThe dumping result looks like (internal data structure):\n\n```text\n  app.                          \u003cB\u003e\n    d                           \u003cB\u003e\n      ebug                      \u003cL\u003e app.debug =\u003e false\n      ump                       \u003cL\u003e app.dump =\u003e 3\n    verbose                     \u003cL\u003e app.verbose =\u003e true\n    logging.                    \u003cB\u003e\n      file                      \u003cL\u003e app.logging.file =\u003e /tmp/1.log\n      rotate                    \u003cL\u003e app.logging.rotate =\u003e 6\n      words                     \u003cL\u003e app.logging.words =\u003e [a 1 false]\n      keys                      \u003cL\u003e app.logging.keys =\u003e map[a:3.13 1.73:zz false:true]\n    server.start                \u003cL\u003e app.server.start =\u003e 5\n    bool                        \u003cL\u003e app.bool =\u003e [on,off,   true] // remarks here | tag = [on off true] ~ a bool slice\n```\n\nAs you seen, the internal structure will be printed out for the deep researching.\n\n![image-20240221115843477](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/image-20240221115843477.png)\n\n\u003e `\u003cB\u003e` is branch, `\u003cL\u003e` is leaf.\n\u003e\n\u003e Leaf node contains data, comment, description and tag (any value).\n\u003e\n\u003e To speed up the tree, any delimiter char is a part of the path.\n\nThe `store` provides advanced APIs to extract the typed data from some a node,\n\n```go\niData := conf.MustInt(\"app.logging.rotate\")\nstringData := conf.MustString(\"app.logging.rotate\")\ndebugMode := conf.MustBool(\"app.debug\")\n...\n```\n\nThe searching tools are also used to locate whether a key exists or not:\n\n```go\nfound := conf.Has(\"app.logging.rotate\")\nnode, isBranch, isPartialMatched, found := conf.Locate(\"app.logging.rotate\")\nt.Logf(\"%v | %s | %v |     | %v, %v, found\", node.Data(), node.Comment(), node.Tag(), isBranch, isPartialMatched, found)\n```\n\n`Locate` is a more friendly `Has` test for the developers when they want to inspect more extra information after searching.\n\nFor more information, browse these public sites:\n\n- \u003chttps://pkg.go.dev/github.com/hedzr/store\u003e\n- \u003chttps://github.com/hedzr/store\u003e\n- Check out the codes in test source files\n\nTo see the recently changes at [CHANGELOG](https://github.com/hedzr/store/blob/master/CHANGELOG).\n\n\u003e Since v1.1.0, unexported struct ptr (*storeS) removed from `Store` API.\n\u003e\n\u003e These apis changed to:\n\u003e\n\u003e - `Clone() (newStore Store)`\n\u003e - `Dup() (newStore Store)`\n\u003e - `WithPrefix(prefix ...string) (newStore Store)`\n\u003e - `WithPrefixReplaced(newPrefix ...string) (newStore Store)`\n\u003e\n\u003e Since v1.2.0, the prototypes of `Locate`/`Query` are changed.\n\u003e\n\u003e - an extra `kvpair` will be returned if there is `:ident` in trie path and matched ok.\n\u003e - support these url var matching: \"/:id/\", \"/*filepath\"\n\u003e\n\u003e For example, matching `/hello/bob` on a router path pattern `/hello/:name` will get `kvpair = {\"name\":\"bob\"}`, and `/search/any/thing/here` on pattern `/search/*keywords` will get `kvpair = {\"keywords\":\"any/thing/here\"}`.\n\u003e\n\u003e Since v1.2.5, `SetTTL(path, ttl, callback)` added.  \n\u003e Since v1.2.8, `SetTTLFast(node, ttl, callback)` added.  \n\u003e Since v1.2.8, `SetEx(path, val, callback)` added.  \n\u003e Since v1.3.0, moved the minimal toolchain to go1.23.7.  \n\u003e Since v1.3.55, moved the minimal toolchain to go1.24.5.  \n\nUsing store as the core of a http router is possible. Since v1.2 we added builtin http route param support (like `:user`).\n\nUsing [hedzr/store](https://github.com/hedzr/store) as a in-memory Cache provider is possible. Since v1.2.5 we added `SetTTL` to reset the data of a node (by key) to zero value. You can inject your codes to drop the key or whatever else.\n\n## More Features\n\nThe `store` gives many advanced features from out of the box, but the relative documents are not enough. We will try our best to fill more documentation at a certain point in the future.\n\nIn short, the `store` can load the data from a provider which will load from its external source, which data will be decoded by a codec decoder.\n\nOnce your configuration data is loaded or inserted into the store manually, you can read them at any time, in any way.\nThat is, the original items can be extracted with a different data type if they can convert smoothly. For example, a string item `3d3h3s` can be got as a `time.Duration` value (via `MustDuration(path)`).\n\nIn totally:\n\n- access fastly to the hierarchical tree data\n- app configurations management\n- load app configs from external sources\n  - various codec translators and source providers\n- merge config fragment\n- save the modifications to alternated external source\n  - see [cmdr-loaders](https://github.com/hedzr/cmdr-laoders)\n  - `$(PWD)/.app.toml` is the target\n- standalone in-memory key-value pair\n- as a in memory cache provider\n- as a kernel of a http-router\n- highly customizable\n\n### Retrieve Node Data\n\nA config entry, so-called as a node (in our Trie-tree), can be retrieved as a typed value:\n\n```go\nfunc ExampleStoreS_Get() {\n    trie := newBasicStore()\n    fmt.Println(trie.MustInt(\"app.dump\"))\n    fmt.Println(trie.MustString(\"app.dump\"))\n    fmt.Println(trie.MustBool(\"app.dump\")) // convert 3 to bool will get false, only 1 -\u003e true.\n    // Output:\n    // 3\n    // 3\n    // false\n}\n\nfunc newBasicStore(opts ...Opt) *storeS {\n    conf := New(opts...)\n    conf.Set(\"app.debug\", false)\n    conf.Set(\"app.verbose\", true)\n    conf.Set(\"app.dump\", 3)\n    conf.Set(\"app.logging.file\", \"/tmp/1.log\")\n    conf.Set(\"app.server.start\", 5)\n\n    ss := conf.WithPrefix(\"app.logging\")\n    ss.Set(\"rotate\", 6)\n    ss.Set(\"words\", []any{\"a\", 1, false})\n    return conf\n}\n```\n\n### Extract A Subset\n\n`GetM(path, opts...) map[string]any` is a power tool to extract the nodes as a map, which has the flattened keys. The extracted result looks like:\n\n```bash\nstore_test.go:150: whole tree: map[app.debug:false app.dump:3 app.logging.file:/tmp/1.log app.logging.rotate:6 app.logging.words:[a 1 false] app.server.start:5 app.verbose:true]\nstore_test.go:160: app.logging sub-tree: map[app.logging.file:/tmp/1.log app.logging.rotate:6 app.logging.words:[a 1 false]]\n```\n\nThe test code is:\n\n```go\nfunc TestStore_GetM(t *testing.T) {\n    conf := newBasicStore()\n\n    m, err := conf.GetM(\"\")\n    if err != nil {\n        t.Fatalf(\"wrong in calling GetM(\\\"\\\"): %v\", err)\n    }\n    t.Logf(\"whole tree: %v\", m)\n\n    // filter by a functor\n\n    m, err = conf.GetM(\"\", WithFilter[any](func(node radix.Node[any]) bool {\n        return strings.HasPrefix(node.Key(), \"app.logging.\")\n    }))\n    if err != nil {\n        t.Fatalf(\"wrong in calling GetM(\\\"\\\"): %v\", err)\n    }\n    t.Logf(\"app.logging sub-tree: %v\", m)\n}\n```\n\n`GetM(\"\")` can extract the whole tree, and `GetM(\"app.logging\")` extract that subtree.\n\nWith filter functor, you can extract `app.logging` subtree by `GetM(\"\", WithFilter[any](func(node radix.Node[any]) bool {\n    return strings.HasPrefix(node.Key(), \"app.logging.\")\n})))`.\n\n### Extract Subtree Into Struct\n\n`GetSectionFrom` makes extracting to struct easier. For example,\n\n```go\nfunc TestStore_GetSectionFrom(t *testing.T) {\n    conf := newBasicStore()\n    conf.Set(\"app.logging.words\", []any{\"a\", 1, false})\n    conf.Set(\"app.server.sites\", -1)\n    t.Logf(\"\\nPath\\n%v\\n\", conf.Dump())\n\n    type loggingS struct {\n        File   uint\n        Rotate uint64\n        Words  []any\n    }\n\n    type serverS struct {\n        Start int\n        Sites int\n    }\n\n    type appS struct {\n        Debug   int\n        Dump    int\n        Verbose int64\n        Logging loggingS\n        Server  serverS\n    }\n\n    type cfgS struct {\n        App appS\n    }\n\n    var ss cfgS\n    err := conf.GetSectionFrom(\"\", \u0026ss) // extract the whole tree\n    t.Logf(\"cfgS: %v | err: %v\", ss, err)\n\n    assertEqual(t, []any{\"a\", 1, false}, ss.App.Logging.Words)\n    assertEqual(t, -1, ss.App.Server.Sites)\n\n    if !reflect.DeepEqual(ss.App.Logging.Words, []any{\"a\", 1, false}) {\n        t.Fail()\n    }\n}\n```\n\n\u003e TODO: Transferring a struct into the `Store` isn't in our plan yet.\n\n### Light-weight Sub-tree\n\nThe `store` has a dead lightweight subtree accessor. By using `WithPrefix` or `WithPrefixReplaced`, you can construct a subtree accessor and read/write a node:\n\n```go\nfunc TestStore_WithPrefix(t *testing.T) {\n    trie := newBasicStore()\n    t.Logf(\"\\nPath\\n%v\\n\", trie.Dump())\n\n    assertEqual(t, 6, trie.MustGet(\"app.logging.rotate\"))\n    conf := trie.WithPrefix(\"app\")\n    assertEqual(t, 6, conf.MustGet(\"logging.rotate\"))\n  \n    conf = conf.WithPrefix(\"logging\")\n    assertEqual(t, 6, conf.MustGet(\"rotate\"))\n    \n  conf = trie.WithPrefixReplaced(\"app.logging\")\n    assertEqual(t, 6, conf.MustGet(\"rotate\"))\n}\n```\n\n### Easily Cutting\n\nBy using `SetPrefix(prefix)`, a `store` and its whole subtree can be moved or associated with a new hierarchical tree structure.\n\nBy using `Dup` or `Clone`, and `Merge`, the `store` can be cut and layout.\n\n### Split key with delimiter\n\nIf a key contains delimiter, it will be split and insert into the `Store`. Technically, the inserter doesn't do special stuff for this key, but the getter will access the tree path separated by the delimiter char.\n\nSo when you're loading a YAML file (or others), the dotted key can make the file consicer:\n\n```yaml\napp.demo.working: \"~/demo\"\n```\n\nIt equals\n\n```yaml\napp:\n  demo:\n    working: \"~/demo\"\n```\n\nThis feature is builtin and cannot disable, due to we have a Trie-tree store and the node is always recognized at extracting.\n\nA side effect is, when you're using a float-point number as a key, that will have matters. Our tip is, don't do that.\n\n### Decompound Map\n\nThe data can be split and orchestrated into tree structure when you're inserting a map.\n\nThis feature works when a provider is loading its external source. `Set(k, v)` doesn't decompound anything in `v`. But `Merge(k, m)` does:\n\n```go\nfunc TestDecompoundMap(t *testing.T) {\n    conf := newBasicStore()\n\n    conf.Set(\"app.map\", false) // ensure key path 'app.map' has already existed\n    // and now merge a map into the point/node\n    err := conf.Merge(\"app.map\", map[string]any{\n        \"k1\": 1,\n        \"k2\": false,\n        \"m3\": map[string]any{\n            \"bobo\": \"joe\",\n        },\n    })\n\n    if err != nil {\n        t.Fatalf(\"Merge failed: %v\", err)\n    }\n\n    assert.Equal(t, int(1), conf.MustGet(\"app.map.k1\"))\n    assert.Equal(t, false, conf.MustGet(\"app.map.k2\"))\n    assert.Equal(t, \"joe\", conf.MustGet(\"app.map.m3.bobo\"))\n}\n```\n\nOf course, it shall be a valid deep `map[string]any`.\n\n### Decompound Slice\n\nA slice can be decompounded once you enabled `WithStoreFlattenSlice(true)`.\n\nIt works for loading an external source, similar like Decompounding Map.\n\n\u003cdetails\u003e\u003csummary\u003eSee the sample code for collapsed sections\u003c/summary\u003e\n\nFor example:\n\n```go\nfunc TestHjson(t *testing.T) {\n    s := store.New()\n    parser := hjson.New()\n    if err := s.Load(context.TODO(),\n        store.WithStorePrefix(\"app.hjson\"),\n        store.WithCodec(parser),\n        store.WithProvider(file.New(\"../testdata/6.hjson\")),\n\n        store.WithStoreFlattenSlice(true),\n    ); err != nil {\n        t.Fatalf(\"Load failed: %v\", err)\n    }\n    t.Logf(\"\\n%-32sData\\n%v\\n\", \"Path\", s.Dump())\n\n    assert.Equal(t, `r.Header.Get(\"From\")`, s.MustGet(\"app.hjson.messages.0.placeholders.0.expr\"))\n    assert.Equal(t, `r.Header.Get(\"User-Agent\")`, s.MustGet(\"app.hjson.messages.1.placeholders.0.expr\"))\n}\n```\n\nThe supplied hjson file has the following contents:\n\n```json5\n{\n  \"language\": \"zh\",\n  \"messages\": [\n    {\n      \"id\": \"Hello {From}!\",\n      \"message\": \"Hello {From}!\",\n      \"translation\": \"\",\n      \"placeholders\": [\n        {\n          \"id\": \"From\",\n          \"string\": \"%[1]s\",\n          \"type\": \"string\",\n          \"underlyingType\": \"string\",\n          \"argNum\": 1,\n          \"expr\": \"r.Header.Get(\\\"From\\\")\"\n        }\n      ]\n    },\n    {\n      \"id\": \"Do you like your browser ({User_Agent})?\",\n      \"message\": \"Do you like your browser ({User_Agent})?\",\n      \"translation\": \"\",\n      \"placeholders\": [\n        {\n          \"id\": \"User_Agent\",\n          \"string\": \"%[1]s\",\n          \"type\": \"string\",\n          \"underlyingType\": \"string\",\n          \"argNum\": 1,\n          \"expr\": \"r.Header.Get(\\\"User-Agent\\\")\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n\u003c/details\u003e\n\n### Notable Nodes\n\nDifferent from other configuration managers, the store is not only a memory key-value store. The nodes in store are both notable and taggable.\n\n```go\nconf.Set(\"debug\", false)\nconf.SetComment(\"debug\", \"a flag to identify app debug mode\", \"remarks here\")\nconf.SetTag(\"debug\", map[string]any{\n    \"handler\": func(){},\n})\n\nnode, _, _, found := conf.Locate(\"debug\")\nif found {\n    t.Log(node.Tag(), node.Description(), node.Comment())\n}\nt.Log(conf.Dump())\n```\n\n`Dump()` will produce the detailed output.\n\n\n\n### Walk The Whole Tree\n\n`Walk(path)` gives a way to iterator the `Store`.\n\n```go\nfunc TestStore_Walk(t *testing.T) {\n    var conf Store = newBasicStore()\n    conf.Walk(\"\", func(path, fragment string, node radix.Node[any]) {\n        t.Logf(\"%v / %v =\u003e %v\", path, fragment, node)\n    })\n}\n\n// Output:\n//  /  =\u003e \u0026{[]  [0xc0000e23f0] \u003cnil\u003e   \u003cnil\u003e 0}\n// app. / app. =\u003e \u0026{[97 112 112 46] app. [0xc0000e2480 0xc0000e2510 0xc0000e26c0 0xc0000e2750] false   \u003cnil\u003e 0}\n// app.d / d =\u003e \u0026{[100] app.d [0xc0000e25a0 0xc0000e2630] false   \u003cnil\u003e 0}\n// app.debug / ebug =\u003e \u0026{[101 98 117 103] app.debug [] false   \u003cnil\u003e 13}\n// app.dump / ump =\u003e \u0026{[117 109 112] app.dump [] 3   \u003cnil\u003e 13}\n// app.verbose / verbose =\u003e \u0026{[118 101 114 98 111 115 101] app.verbose [] true   \u003cnil\u003e 13}\n// app.logging. / logging. =\u003e \u0026{[108 111 103 103 105 110 103 46] app.logging. [0xc0000e2870 0xc0000e2900 0xc0000e2990] /tmp/1.log   \u003cnil\u003e 0}\n// app.logging.file / file =\u003e \u0026{[102 105 108 101] app.logging.file [] /tmp/1.log   \u003cnil\u003e 13}\n// app.logging.rotate / rotate =\u003e \u0026{[114 111 116 97 116 101] app.logging.rotate [] 6   \u003cnil\u003e 13}\n// app.logging.words / words =\u003e \u0026{[119 111 114 100 115] app.logging.words [] [a 1 false]   \u003cnil\u003e 13}\n// app.server.start / server.start =\u003e \u0026{[115 101 114 118 101 114 46 115 116 97 114 116] app.server.start [] 5   \u003cnil\u003e 13}\n```\n\nAs a feature based on Trie-tree, `Walk(\"app\")` will walk from the parent of `app.` node. And `Walk(\"app.\")` will walk from the `app.` node.\n\nLike `GetM`, passing \"\" will walk from the top-level root node.\n\n### Modified State\n\nEach node has a modified state, so we can extract them from the `Store`:\n\n```go\nfunc (s *loadS) Save(ctx context.Context) (err error) { return s.trySave(ctx) }\nfunc (s *loadS) trySave(ctx context.Context) (err error) {\n    if s.codec != nil {\n        var m map[string]any\n        if m, err = s.GetM(\"\", WithFilter[any](func(node radix.Node[any]) bool {\n            return node.Modified()\n        })); err == nil {\n            var data []byte\n            if data, err = s.codec.Marshal(m); err == nil {\n                switch fp := s.provider.(type) {\n                case OnceProvider:\n                    err = fp.Write(data)\n                default:\n                    err = ErrNotImplemented\n                }\n\n                if errors.Is(err, ErrNotImplemented) {\n                    if wr, ok := s.provider.(io.Writer); ok {\n                        _, err = wr.Write(data)\n                    }\n                }\n            }\n        }\n    }\n    return\n}\n```\n\nWe assume the user calling `Set(k, v)` will cause modified state was set to true. And app loading and merging to the `Store` at startup will be treated as initial state, so the modified state keeps unset (i.e., false).\n\n### Provider for External Source\n\n`Provider`s could be used to describe an external source, such as file, env, or consul and vice versa.\n\n`Codec`s are used to describe how to decode a streaming input loaded by Provider, such as yaml, toml, json, hcl, etc.\n\nA loading logic typically is:\n\n```go\nfunc TestTOML(t *testing.T) {\n    s := store.New()\n    parser := toml.New()\n    if err := s.Load(context.TODO(),\n        store.WithStorePrefix(\"app.toml\"),\n        store.WithCodec(parser),\n        store.WithProvider(file.New(\"../testdata/5.toml\")),\n\n        store.WithStoreFlattenSlice(true),\n    ); err != nil {\n        t.Fatalf(\"Load failed: %v\", err)\n    }\n    t.Logf(\"\\n%-32sData\\n%v\\n\", \"Path\", s.Dump())\n\n    assert.Equal(t, `127.0.0.1`, s.MustGet(\"app.toml.host\"))\n    assert.Equal(t, `TLS 1.3`, s.MustGet(\"app.toml.TLS.version\"))\n    assert.Equal(t, `AEAD-AES128-GCM-SHA256`, s.MustGet(\"app.toml.TLS.cipher\"))\n    assert.Equal(t, `go`, s.MustGet(\"app.toml.tags.0\"))\n}\n```\n\nMore tests at [tests/*_test.go](./tree/master/tests/) .\n\n### Implement A Provider\n\nA `Provider` should support `Read()`:\n\n```go\ntype Provider interface {\n    Read() (m map[string]any, err error) // return ErrNotImplemented as an identifier\n\n    ProviderSupports\n}\n\ntype ProviderSupports interface {\n    GetCodec() (codec Codec)   // return the bound codec decoder\n    GetPosition() (pos string) // return a position pointed to a trie node path\n    WithCodec(codec Codec)\n    WithPosition(pos string)\n}\n```\n\nYour provider can support `OnceProvider` or `StreamProvider` while its `Read` return `radix.ErrNotImplemented`. OnceProvider assumes the loader read binary content at once. `StreamProvider` allows reading the large content progressively.\n\n### Set TTL for A Key\n\nSince v1.2.5, we added TTL support for a trie node. Once the TTL arrives, the data of the target key will be cleared. Which means, the data field would be reset to zero value (or a nil value for an any type T).\n\nThe code could be:\n\n```go\nfunc TestStore_SetTTL(t *testing.T) {\n\tconf := newBasicStore()\n\tdefer conf.Close()\n\n\tpath := \"app.logging.rotate\"\n\tconf.SetTTL(path, 200*time.Millisecond, func(s *radix.TTL[any], nd radix.Node[any]) {\n\t\tt.Logf(\"%q cleared\", path)\n\t})\n\tpath2 := \"app.logging.file\"\n\tconf.SetTTL(path2, 200*time.Millisecond, func(s *radix.TTL[any], nd radix.Node[any]) {\n\t\tt.Logf(\"%q (%q) cleared\", path2, nd.Data())\n\t})\n\n\ttime.Sleep(450 * time.Millisecond)\n\tassertEqual(t, true, conf.Has(path2))\n\tassertEqual(t, nil, conf.MustGet(path2))\n\tassertEqual(t, 0, conf.MustInt(path))\n}\n```\n\n\n\n## Benchmarks\n\nThe `store` is designed to reduce the allocations much better, and up the performance much better. We have a zero-allocation implementation in reading a key-value pair, currently.\n\nOur benchmark testing (`test/bench_test.go`) shows:\n\n```bash\ngoos: darwin\ngoarch: amd64\npkg: github.com/hedzr/store/tests\ncpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz\nBenchmarkTrieSearch\nBenchmarkTrieSearch/hedzr/storeT[any]\nBenchmarkTrieSearch/hedzr/storeT[any]-16             59983291            18.99 ns/op           0 B/op           0 allocs/op\nBenchmarkTrieSearch/hedzr/store\nBenchmarkTrieSearch/hedzr/store-16                   60454639            19.43 ns/op           0 B/op           0 allocs/op\nPASS\n```\n\nSome control groups with the same executive environment produced:\n\n```bash\n...\nBenchmarkTrieSearch/kzzzz\nBenchmarkTrieSearch/kzzzz-16                         46264582            28.88 ns/op          16 B/op           1 allocs/op\nBenchmarkTrieSearch/vzzzz\nBenchmarkTrieSearch/vzzzz-16                         22824562            51.21 ns/op          32 B/op           2 allocs/op\n...\n```\n\n\u003e 1. To avoid controversy, pkg-name masked.\n\u003e\n\u003e 2. Both of these testing data sets have the same scale basically (lower than 20 keys). Also, the querying words are same.\n\u003e\n\u003e    ![Screenshot 2024-02-22 at 10.55.13](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/Screenshot%202024-02-22%20at%2010.55.13.png)\n\u003e\n\u003e 3. No Warrenties.\n\nThe performance benefits mainly from the refresh implemented about our internal Trie-tree (radix-tree).\n\nAs an addition, here are huger/larger benches:\n\n```bash\nTest updated:\ngoos: darwin\ngoarch: arm64\npkg: github.com/hedzr/go-zapall/triebench\ncpu: Apple M3 Pro\n    ./triebench/bench_test.go:35: store/normal keys: 104 keys\n    ./triebench/bench_test.go:37: kxxxx/normal keys: 102 keys\n    ./triebench/bench_test.go:39: bxxxxxxx/normal keys: 106 keys\n    ./triebench/bench_test.go:55: store/Huge keys: 422 keys\n    ./triebench/bench_test.go:57: kxxxx/Huge keys: 318 keys\nBenchmarkTrieSearch/hedzr/storeT[any]\nBenchmarkTrieSearch/hedzr/storeT[any]-11         \t96411034\t        12.25 ns/op\t       9 B/op\t       0 allocs/op\nBenchmarkTrieSearch/hedzr/store\nBenchmarkTrieSearch/hedzr/store-11               \t40387551\t        29.89 ns/op\t       9 B/op\t       0 allocs/op\nBenchmarkTrieSearch/hedzr/store/Huge\nBenchmarkTrieSearch/hedzr/store/Huge-11          \t24667755\t        50.84 ns/op\t     222 B/op\t       0 allocs/op\nBenchmarkTrieSearch/kxxxx\nBenchmarkTrieSearch/kxxxx-11                     \t32698178\t        36.40 ns/op\t      16 B/op\t       1 allocs/op\nBenchmarkTrieSearch/kxxxx/Huge\nBenchmarkTrieSearch/kxxxx/Huge-11                \t11557538\t       106.6 ns/op\t     459 B/op\t      12 allocs/op\nBenchmarkTrieSearch/bxxxe\nBenchmarkTrieSearch/bxxxe-11                     \t17893599\t        66.24 ns/op\t      88 B/op\t       2 allocs/op\nBenchmarkTrieSearch/vxxxx\nBenchmarkTrieSearch/vxxxx-11                     \t22784323\t        53.80 ns/op\t      32 B/op\t       2 allocs/op\n\nTest again:\n    ./triebench/bench_test.go:35: store/normal keys: 105 keys\n    ./triebench/bench_test.go:37: kxxxx/normal keys: 103 keys\n    ./triebench/bench_test.go:39: bxxxxxxx/normal keys: 106 keys\n    ./triebench/bench_test.go:55: store/Huge keys: 422 keys\n    ./triebench/bench_test.go:57: kxxxx/Huge keys: 318 keys\nBenchmarkTrieSearch/hedzr/storeT[any]\nBenchmarkTrieSearch/hedzr/storeT[any]-11         \t86327053\t        11.59 ns/op\t       8 B/op\t       0 allocs/op\nBenchmarkTrieSearch/hedzr/store\nBenchmarkTrieSearch/hedzr/store-11               \t38669240\t        29.97 ns/op\t       8 B/op\t       0 allocs/op\nBenchmarkTrieSearch/hedzr/store/Huge\nBenchmarkTrieSearch/hedzr/store/Huge-11          \t24919380\t        49.81 ns/op\t     221 B/op\t       0 allocs/op\nBenchmarkTrieSearch/kxxxx\nBenchmarkTrieSearch/kxxxx-11                     \t32515147\t        36.97 ns/op\t      16 B/op\t       1 allocs/op\nBenchmarkTrieSearch/kxxxx/Huge\nBenchmarkTrieSearch/kxxxx/Huge-11                \t12414290\t        96.13 ns/op\t     411 B/op\t      11 allocs/op\nBenchmarkTrieSearch/bxxxx\nBenchmarkTrieSearch/bxxxx-11                     \t17892199\t        67.23 ns/op\t      89 B/op\t       2 allocs/op\nBenchmarkTrieSearch/vxxxx\nBenchmarkTrieSearch/vxxxx-11                     \t22975539\t        53.40 ns/op\t      32 B/op\t       2 allocs/op\n\n```\n\n\u003e This benchmark tests ruled out the effects from consutructing the config set, and do a large numbers of `GET` a key as bench result.\n\u003e UPDATED at 2025-07-03 with both of their recent versions.\n\nYou can find out that our `store` has a better score while working on a large configuration set,\nalthough it might take more latency than on a tiny set.\n\n\u003e The datasource of huge test is a pet-store openapi swagger doc, coming from \u003chttps://editor.swagger.io/\u003e.\n\u003e With a same input like a YAML file, the `store` could get more key-value pairs because `store.WithStoreFlattenSlice(true)` applied, which will expand slices and maps in a value as nested key-value pairs.\n\n## Dependencies\n\nThe `store`[^1] imports some modules of mine:\n\n1. [`hedzr/evendeep`[^2]]\n2. [`hedzr/logg/slog`[^3]]\n3. [`hedzr/errors.v3`[^4]]\n4. [`hedzr/is`[^5]]\n\nThe dependency graph is:\n\n```mermaid\ngraph BT\n  hzis(hedzr/is)--\u003ehzlogg(hedzr/logg/slog)\n  hzis--\u003ehzdiff(hedzr/evendeep)\n  hzlogg--\u003ehzdiff\n  hzerrors(gopkg.in/hedzr/errors.v3)--\u003ehzdiff\n  hzerrors--\u003ehzstore(hedzr/store)\n  hzis--\u003ehzstore(hedzr/store)\n  hzlogg--\u003ehzstore(hedzr/store)\n  hzdiff--\u003ehzstore(hedzr/store)\n```\n\n[^1]: `hedzr/store` is a high-performance configure management library\n[^2]: `hedzr/evendeep` offers a customizable deepcopy tool to you. There are also deepequal, deepdiff tools in it.\n[^3]: `hedzr/logg` provides a slog like and colorful logging library\n[^4]: `hedzr/errors.v3` provides some extensions and compatible layer over go1.11 ~ nowadays.\n[^5]: `hedzr/is` is a basic environ detectors library\n\n## LICENSE\n\nApache 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhedzr%2Fstore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhedzr%2Fstore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhedzr%2Fstore/lists"}