Full-parity Go port of the canonical TypeScript implementation.
For motivation, language-neutral concepts, and the cross-language parity matrix, see the top-level README and REPORT.md.
Module path: github.com/voxgig/struct/go.
go get github.com/voxgig/struct/goimport voxgigstruct "github.com/voxgig/struct/go"package main
import (
"fmt"
voxgigstruct "github.com/voxgig/struct/go"
)
func main() {
store := map[string]any{
"db": map[string]any{"host": "localhost"},
"user": map[string]any{"first": "Ada", "last": "Lovelace"},
"age": 36,
}
fmt.Println(voxgigstruct.GetPath(store, "db.host"))
// localhost
out, _ := voxgigstruct.Transform(store, map[string]any{
"name": "`user.first`",
"surname": "`user.last`",
"years": "`age`",
})
fmt.Println(out)
// map[name:Ada surname:Lovelace years:36]
}Go exports identifiers in PascalCase, so canonical lowercase function names are uppercased:
| Canonical | Go |
|---|---|
getpath |
GetPath |
setpath |
SetPath |
getprop |
GetProp |
setprop |
SetProp |
isnode |
IsNode |
keysof |
KeysOf |
escre |
EscRe |
escurl |
EscUrl |
All other names follow the same rule.
Source: voxgigstruct.go. Package
voxgigstruct.
func IsNode(val any) bool
func IsMap(val any) bool
func IsList(val any) bool
func IsKey(val any) bool
func IsEmpty(val any) bool
func IsFunc(val any) boolvoxgigstruct.IsNode(map[string]any{"a": 1}) // truevoxgigstruct.IsMap(map[string]any{"a": 1}) // truevoxgigstruct.IsList([]any{1, 2}) // truevoxgigstruct.IsKey("name") // truevoxgigstruct.IsEmpty([]any{}) // truevoxgigstruct.IsMap([]any{1}) // false
voxgigstruct.IsKey("") // false
voxgigstruct.IsEmpty(nil) // true
voxgigstruct.IsFunc(func() {}) // truefunc Typify(value any) int
func Typename(t int) stringvoxgigstruct.Typify(1) // T_scalar | T_number | T_integer (201326720)voxgigstruct.Typify(42) // T_scalar | T_number | T_integer
voxgigstruct.Typify("hi") // T_scalar | T_string
voxgigstruct.Typify(nil) // T_scalar | T_nullvoxgigstruct.Typename(8192) // "map" (8192 == T_map)voxgigstruct.Typename(voxgigstruct.Typify("hi")) // "string"func Size(val any) int
func Slice(val any, args ...any) any
func Pad(str any, args ...any) stringargs carries optional (start, end, mutate) for Slice and
(padding, padchar) for Pad.
voxgigstruct.Size([]any{1, 2, 3}) // 3Slice(val, start, end) takes a start offset (a positive start drops the
first start items, so ("abcdef", 2) returns "cdef"); a negative start
counts from the end and drops the last |start| items (so ("abcdef", -3)
keeps the first three), and end is exclusive:
voxgigstruct.Slice([]any{1, 2, 3, 4, 5}, 1, 4) // []any{2, 3, 4}voxgigstruct.Slice("abcdef", -3) // "abc" (drops the last 3)Pad right-pads to the target width with spaces by default:
voxgigstruct.Pad("a", 3) // "a "voxgigstruct.Pad("hi", 5) // "hi "
voxgigstruct.Pad("hi", -5, "*") // "***hi"func GetProp(val any, key any, alts ...any) any
func SetProp(parent any, key any, val any) any
func DelProp(parent any, key any) any
func GetElem(val any, key any, alts ...any) any
func GetDef(val any, alt any) any
func HasKey(val any, key any) bool
func KeysOf(val any) []string
func Items(val any) [][2]any
func ItemsApply(val any, apply func([2]any) any) []any
func StrKey(key any) stringvoxgigstruct.GetProp(map[string]any{"x": 1}, "x") // 1voxgigstruct.SetProp(map[string]any{"a": 1}, "b", 2) // map[a:1 b:2]voxgigstruct.DelProp(map[string]any{"a": 1, "b": 2}, "a") // map[b:2]voxgigstruct.GetElem([]any{10, 20, 30}, -1) // 30voxgigstruct.HasKey(map[string]any{"a": 1}, "a") // truevoxgigstruct.Items(map[string]any{"a": 1, "b": 2}) // [[a 1] [b 2]]voxgigstruct.StrKey(2.2) // "2"voxgigstruct.GetProp(map[string]any{}, "b", "fallback") // "fallback"
voxgigstruct.GetDef(nil, "fb") // "fb"
voxgigstruct.StrKey(1) // "1"KeysOf returns map keys sorted:
voxgigstruct.KeysOf(map[string]any{"b": 4, "a": 5}) // [a b] (sorted)func GetPath(store any, path any, injdefs ...*Injection) any
func SetPath(store any, path any, val any, injdefs ...map[string]any) any
func Pathify(val any, args ...any) stringvoxgigstruct.GetPath(
map[string]any{"a": map[string]any{"b": map[string]any{"c": 42}}},
"a.b.c",
)
// 42voxgigstruct.GetPath(map[string]any{"a": []any{10, 20}}, "a.1")
// 20
store := map[string]any{}
voxgigstruct.SetPath(store, "db.host", "localhost")
// store == map[db:map[host:localhost]]voxgigstruct.SetPath(map[string]any{"a": 1, "b": 2}, "b", 22) // map[a:1 b:22]voxgigstruct.Pathify([]any{"a", "b", "c"}) // "a.b.c"func Walk(val any, apply WalkApply, opts ...any) any
func WalkDescend(val any, apply WalkApply, key *string, parent any,
path []string) any
func Merge(val any, maxdepths ...int) any
func Clone(val any) any
func CloneFlags(val any, flags map[string]bool) any
func Flatten(list any, depths ...int) any
func Filter(val any, check func([2]any) bool) []any
type WalkApply func(key *string, val any, parent any, path []string) anykey is a *string — nil at the root, otherwise the map key or the
string form of a list index. Walk's opts carry the optional after
callback and maxdepth; WalkDescend starts a descent from a non-root
position with an explicit key, parent, and starting path.
voxgigstruct.Walk(tree, func(k *string, v, p any, path []string) any {
if v == nil { return "DEFAULT" }
return v
})Last input wins; maps deep-merge; lists merge by index:
voxgigstruct.Merge([]any{
map[string]any{"a": 1, "b": 2, "k": []any{10, 20}, "x": map[string]any{"y": 5, "z": 6}},
map[string]any{"b": 3, "d": 4, "e": 8, "k": []any{11}, "x": map[string]any{"y": 7}},
})
// map[a:1 b:3 d:4 e:8 k:[11 20] x:map[y:7 z:6]]voxgigstruct.Clone(map[string]any{"a": map[string]any{"b": []any{1, 2}}})
// map[a:map[b:[1 2]]] (a deep copy)voxgigstruct.Flatten([]any{1, []any{2, []any{3}}}) // []any{1, 2, []any{3}} (one level by default)Filter passes each [key, value] pair to the check and returns the
matching values (not the pairs):
voxgigstruct.Filter([]any{1, 2, 3, 4, 5},
func(kv [2]any) bool { return kv[1].(int) > 3 })
// []any{4, 5}func EscRe(s string) string
func EscUrl(s string) string
func Join(arr []any, args ...any) string
func JoinUrl(parts []any) string
func Jsonify(val any, flags ...map[string]any) string
func Stringify(val any, args ...any) stringvoxgigstruct.EscRe("a.b+c") // "a\\.b\\+c"voxgigstruct.EscUrl("hello world?") // "hello%20world%3F"voxgigstruct.Join([]any{"a", "b", "c"}, "/") // "a/b/c"voxgigstruct.JoinUrl([]any{"http:", "/foo/", "/bar"})
// "http:/foo/bar"Jsonify pretty-prints by default (indent 2); pass {"indent": 0} for the
compact form:
voxgigstruct.Jsonify(map[string]any{"a": 1})
// {
// "a": 1
// }voxgigstruct.Jsonify(map[string]any{"a": 1, "b": 2}, map[string]any{"indent": 0})
// {"a":1,"b":2}Stringify is the compact, quote-light form — keys are sorted and object
braces are kept; a second argument caps the length (the ... counts):
voxgigstruct.Stringify(map[string]any{"a": 1, "b": []any{2, 3}}) // "{a:1,b:[2,3]}"voxgigstruct.Stringify("verylongstring", 5) // "ve..."func Inject(val any, store any, injdefs ...*Injection) any
func Transform(data any, spec any, injdefs ...*Injection) (any, error)
func TransformModify(data any, spec any, extra any, modify Modify) any
func TransformModifyHandler(data any, spec any, extra any, modify Modify,
handler Injector, errs *ListRef[any],
meta map[string]any) any
func TransformCollect(data any, spec any) (any, []string)
func Validate(data any, spec any, injdefs ...*Injection) (any, error)
func Select(children any, query any) []any// Backtick refs in strings are replaced by store values.
voxgigstruct.Inject(
map[string]any{"x": "`a`", "y": 2},
map[string]any{"a": 1},
)
// map[x:1 y:2]voxgigstruct.Inject(
map[string]any{"greeting": "hello `name`"},
map[string]any{"name": "Ada"},
)
out, _ := voxgigstruct.Transform(
map[string]any{"hold": map[string]any{"x": 1}, "top": 99},
map[string]any{"a": "`hold.x`", "b": "`top`"},
)
// out == map[a:1 b:99]// Validate against a shape (returns the data, or an error on mismatch).
out, err := voxgigstruct.Validate(
map[string]any{"name": "Ada", "age": 36},
map[string]any{"name": "`$STRING`", "age": "`$INTEGER`"},
)
// out == map[age:36 name:Ada]// Find children matching a query.
voxgigstruct.Select(
map[string]any{
"a": map[string]any{"name": "Alice", "age": 30},
"b": map[string]any{"name": "Bob", "age": 25},
},
map[string]any{"age": 30},
)
// [map[$KEY:a age:30 name:Alice]]Transform commands drive structural ops. A command like $EACH appears in
value position — as the first element of a list
[]any{"$EACH", path, subspec} — mapping the sub-spec over every entry at
path:
out, _ := voxgigstruct.Transform(
map[string]any{"v": 1, "a": []any{map[string]any{"q": 13}, map[string]any{"q": 23}}},
map[string]any{"x": map[string]any{"y": []any{"`$EACH`", "a",
map[string]any{"q": "`$COPY`", "r": "`.q`", "p": "`...v`"}}}},
)
// out == map[x:map[y:[map[p:1 q:13 r:13] map[p:1 q:23 r:23]]]]Putting a command in key position (or, for $APPLY, directly under a
map) is an error — commands must be list values:
_, err := voxgigstruct.Transform(map[string]any{}, map[string]any{"x": "`$APPLY`"})
// err: $APPLY: invalid placement in parent map, expected: list.func Jm(args ...any) map[string]any // JSON Object
func Jt(args ...any) []any // JSON Tuple/Arrayvoxgigstruct.Jm("a", 1, "b", 2) // map[a:1 b:2]
voxgigstruct.Jt(1, 2, 3) // [1 2 3]func CheckPlacement(modes int, ijname string, parentTypes int,
inj *Injection) bool
func InjectorArgs(argTypes []int, args []any) []any
func InjectChild(child any, store any, inj *Injection) *InjectionGeneric wrapper providing pointer-stable list semantics. Used
internally by merge and inject; you only need it when writing
custom modify callbacks that mutate lists.
ref := &voxgigstruct.ListRef[int]{List: []int{1, 2, 3}}voxgigstruct.SKIP // emit nothing for this key
voxgigstruct.DELETE // remove this key from the parent
voxgigstruct.NOVAL // no value at all — canonical's `undefined`NOVAL is this port's third state. Go has nil and it has real values and
nothing between, so Typify(nil) is T_scalar | T_null and T_noval was
otherwise unreachable — the port could not express typify() as distinct
from typify(null), two results the corpus pins as different. Typify
recognises NOVAL ahead of its reflect dispatch; everywhere else the port
follows canonical, which treats absent and null alike (null == val is true
for both in JavaScript).
const (
voxgigstruct.T_any
voxgigstruct.T_noval
voxgigstruct.T_boolean
voxgigstruct.T_decimal
voxgigstruct.T_integer
voxgigstruct.T_number
voxgigstruct.T_string
voxgigstruct.T_function
voxgigstruct.T_symbol
voxgigstruct.T_null
voxgigstruct.T_list
voxgigstruct.T_map
voxgigstruct.T_instance
voxgigstruct.T_scalar
voxgigstruct.T_node
)voxgigstruct.M_KEYPRE
voxgigstruct.M_KEYPOST
voxgigstruct.M_VAL
voxgigstruct.MODENAME // []string mapping flags to names
voxgigstruct.PLACEMENT // placement helpers$DELETE $COPY $KEY $META $ANNO
$MERGE $EACH $PACK $REF $FORMAT $APPLY
$MAP $LIST $STRING $NUMBER $INTEGER $DECIMAL $BOOLEAN
$NULL $NIL $FUNCTION $INSTANCE $ANY $CHILD $ONE $EXACT
Go has only nil. JSON null and "absent" both map to nil at the
user-facing API. Where the test corpus needs to distinguish them,
the test runner uses string sentinels __NULL__ and __UNDEF__.
Go has no optional or named parameters. Where canonical TypeScript takes options, the Go port either:
- collects them in a variadic (e.g.
Pad(str, args ...any),Walk(val, apply, opts ...any)whereoptscarry the optionalaftercallback andmaxdepth); or - exposes a separate function (e.g.
WalkDescendfor an ad-hoc descent from a non-root position;CloneFlagsfor clone-with-options).
Walk is the short form. WalkDescend(val, apply, key, parent, path)
starts a recursive descent from a non-root position, taking an explicit
key, parent, and starting path.
Transform and Validate return (any, error), matching Go's idiom for
fallible operations. The error carries a human-readable message describing
the first problem — an unknown $FORMAT, a type mismatch — or an aggregate,
if the underlying call collected several. Canonical throws at exactly that
point; this is the Go form of the same behaviour.
Supplying your own error collector (an *Injection with Errs set, or
TransformCollect) means the errors are yours to inspect, and neither call
generates one. TransformModify and TransformModifyHandler return a
plain any; TransformCollect returns (any, []string) — the data plus
any collected error strings.
Go slices are values: appending may allocate a new backing array,
breaking pointer-stability. ListRef[T] is a thin generic wrapper
that gives every holder a stable reference -- preserving the
canonical "lists are reference-stable" assumption.
92/92 tests pass against the shared corpus.
Uniform six-function regex API (see /design/REGEX_API.md). The Go port
wraps the stdlib regexp package — Go's regexp is the RE2
reference implementation.
| Function | Maps to |
|---|---|
ReCompile(pattern) |
regexp.MustCompile(pattern) (panics on bad pattern) |
ReTest(pattern, input) |
re.MatchString(input) |
ReFind(pattern, input) |
re.FindStringSubmatch(input) |
ReFindAll(pattern, input) |
re.FindAllStringSubmatch(input, -1) |
ReReplace(pattern, input, rep) |
re.ReplaceAllString(input, rep) |
ReReplaceFunc(pattern, input,f) |
re.ReplaceAllStringFunc(input, f) |
ReEscape(s) |
alias for EscRe(s) |
Patterns must stay inside the RE2 subset documented in /design/REGEX.md.
Since Go's regexp engine is RE2, this is the natural ceiling: there is
no PCRE escape hatch.
ReCompilepanics. It's a pass-through toregexp.MustCompile, so an invalid pattern aborts viapanic. This matches the throw/raise behaviour of every other port; wrap inrecover()if you accept user-supplied patterns.- Bounded quantifier cap. RE2 refuses
{n,m}withm > 1000.^a{0,10000}b$panics at compile time with "invalid repeat count". This is a hard RE2 limit — no portable workaround. The canonical patterns and$LIKEoperator stay well below it. - No backreferences or lookaround. RE2 does not support them by
design.
^(a+)\1$panics on compile. The cross-port dialect already forbids them; this is the engine that enforces the rule hardest. - Zero-width
re_replaceuses RE2's convention.re_replace("a*", "abc", "X")returns"XbXcX"— RE2 suppresses an empty match immediately after a non-empty match at the same offset. PCRE / ECMA / .NET / Java / the in-tree Thompson ports all return"XXbXcX"instead. This is inherent to Go's host regex package and is not wrapped: portable callers should not depend on cross-port identity of zero-width replacement output.
See /design/REGEX_PATHOLOGICAL.md for the cross-port pathological-input panel.
cd go
go build ./...
go test ./...
# or:
make testTests in voxgigstruct_test.go consume
fixtures from ../build/test/.