NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Go modules · #2137 by repository stars
Last release 22 days ago
16 Sep 2026
Release timing varies
gaps range from 4 weeks to 1.8 years
Most releases are documented
notes for 13 of 15 stable releases
Nothing withdrawn
no release was ever pulled
9 years old
39 releases · first in 2017
v1.10.0 ended with six known issues. This release fixes three of them — data with a cycle crashed the process, a SetDefaults ran after an unmarshaler
v1.10.0 ended with six known issues. This release fixes three of them — data with a cycle crashed the process, a SetDefaults ran after an unmarshaler had taken the tag, and a failed default left part of itself behind — and half of a fourth: Set no longer writes to a map of slices or maps. The other two, and the rest of the fourth, stay as they are, now documented as intended. The API is unchanged, and so is go.mod.
Behavior changes come first, as before. Two of them can change what a working program does, and neither announces itself: one stops a SetDefaults call, and the other changes what Set leaves behind when it returns an error.
SetDefaults is skipped behind a pointer once an unmarshaler took the tag (#97)The README says that when a tag is handed to UnmarshalText, SetDefaults is not called. v1.10.0 made that hold for a struct, behind a pointer or not, and listed the rest as a known issue: a pointer to any other type still got SetDefaults after its unmarshaler took the tag. It no longer does:
type Level int
func (l *Level) UnmarshalText(b []byte) error {
n, err := strconv.Atoi(string(b))
if err != nil {
return err
}
*l = Level(n)
return nil
}
func (l *Level) SetDefaults() { *l += 100 }
type Config struct {
Level *Level `default:"3"` // v1.10.0: 103. v1.11.0: 3.
}a field of that Level, left zero |
v1.10.0 | v1.11.0 |
|---|---|---|
*Level default:"3" |
103 | 3 |
**Level default:"3" |
103 | 3 |
embedded *Level default:"3" |
103 | 3 |
*Level, with UnmarshalJSON in place of UnmarshalText |
103 | 3 |
*Level default:"", which no unmarshaler is offered |
100 | 100 |
*Level default:"0x10", which UnmarshalText rejects and parsing by kind takes |
116 | 116 |
Level default:"3" |
3 | 3 |
Nothing reports the missing call. How to tell whether you are affected: look for a type that is not a struct — a named int, string or slice — with both SetDefaults and UnmarshalText or UnmarshalJSON, behind a pointer a tag allocates. If its SetDefaults adjusted what the unmarshaler produced, that adjustment is gone; make it in the unmarshaler instead. A field of the type itself never got the call, and a pointer the caller allocated is still left alone.
When a default failed, Set returned the error but kept what it had filled on the way down. The value was then no longer zero, so a second Set could skip the tag and return nil, which v1.10.0 listed as a known issue for *scalar, *[]T and *map fields. Now each value Set found zero on the way to the failing default is put back to zero, whatever part of the default it had taken:
after Set returned the error |
v1.10.0 | v1.11.0 |
|---|---|---|
*int default:"eighty" |
&0, and a second Set returns nil |
nil, and a second Set returns the error again |
*[]string default:"[1]" |
&[], and a second Set returns nil |
nil, and a second Set returns the error again |
a struct field default:"{\"X\": 1, \"Y\": \"two\"}" |
{X:1 Y:0} |
{X:0 Y:0} |
an untagged struct field whose A took 7 before B failed |
{A:7 B:0} |
{A:0 B:0} |
big.Int default:"12x", which its UnmarshalText fills partway before rejecting |
12 |
0 |
[]T default:"[{}]", whose element's default fails |
one element | nil |
Only values that were zero are put back. A pointer, slice or map the caller provided is kept, and the fields of the struct passed to Set keep the defaults they took before the failure: there, A stays 7. The *uuid.UUID row in the v1.10.0 notes changes the same way, to an error with the pointer left nil.
Who is affected: code that ignores Set's error, or recovers from MustSet's panic, and then uses the value. Where it found an allocated pointer it now finds nil, so reading through it panics; where it found part of a default it finds a zero value; and a second Set returns the error again, where it could return nil.
Set followed wherever the caller's data pointed, so data with a way back to itself, such as a child pointing up to its parent, was walked until the stack overflowed. That is a fatal error rather than a panic: no recover catches it, and the process exits.
type Node struct {
Name string `default:"node"`
Parent *Node
Children []*Node
}
root := &Node{}
root.Children = []*Node{{Parent: root}}
err := defaults.Set(root)
// v1.10.0: fatal error: stack overflow
// v1.11.0: err is nil, both nodes are named "node", and the parent pointer is keptSet now keeps track of the structs, slices and maps on the path it is walking. Where the data leads back to one of them, it goes no further, and the walk already under way finishes that value, so a struct gets no second SetDefaults from its own cycle. Down to 64 values deep the check scans the path and allocates nothing; below that it builds an index of the path and looks values up in it, so deep data costs a few allocations rather than time growing with the square of the depth (#108). See Performance.
A walk that finished in v1.10.0 finishes the same way, with one exception: if a SetDefaults below a repeated value broke the cycle as a side effect, say by clearing the pointer back, v1.10.0 walked that value a second time, and this release does not. The check covers the current path only, so a value that two paths reach, neither through the other, is still walked on both, as described below.
Set no longer writes to a map of slices or maps (#105)A map value is not addressable, so Set fills a copy and stores it back under its key. It stored back every struct, slice and map value it walked, changed or not, which v1.10.0 listed as a known issue: a goroutine reading the map meanwhile raced with Set, and could crash. A slice or map copy shares its array or table with the value in the map, so storing it back never changed anything, and it no longer happens:
a map[string][]T that Set walks |
v1.10.0 | v1.11.0 |
|---|---|---|
another goroutine reads the map during Set |
data race, which can crash with concurrent map read and map write |
Set only reads the map |
an element's SetDefaults deletes the key its slice is under |
Set stores the key back |
the key stays deleted |
The same holds for a map of maps. A struct value is still stored back, changed or not, so a map of structs still must not be read during Set; that is now documented as intended, below.
An int64-kinded field is offered to time.ParseDuration and then to strconv.ParseInt, since reflection cannot tell a time.Duration from a plain int64 (#66). When both rejected the tag, only ParseInt's error was reported, so a duration with a unit Go does not know read as bad integer syntax:
| v1.10.0 | v1.11.0 | |
|---|---|---|
time.Duration default:"1d" |
strconv.ParseInt: parsing "1d": invalid syntax |
time: unknown unit "d" in duration "1d"; strconv.ParseInt: parsing "1d": invalid syntax |
int64 default:"abc" |
strconv.ParseInt: parsing "abc": invalid syntax |
time: invalid duration "abc"; strconv.ParseInt: parsing "abc": invalid syntax |
int default:"1d" |
strconv.ParseInt: parsing "1d": invalid syntax |
unchanged |
The field X: invalid default "…": prefix is unchanged, and on a plain int64 the duration half is noise, the cost of the shared parser. A type whose own unmarshaler rejected the tag still reports only that rejection (#90). Both errors are wrapped, so errors.As still reaches the *strconv.NumError and errors.Is still matches strconv.ErrSyntax and strconv.ErrRange, but errors.Unwrap no longer reaches it in one step: errors.Unwrap(err).(*strconv.NumError) stops matching. An empty tag is still no error, and no longer pays for an error message it throws away (#107).
Three behaviors that look wrong were weighed and kept. The README now describes each, and a test marked // QUIRK or // BUG pins it, so changing one later means flipping that test:
SetDefaults call on each, as v1.10.0's known issues said. #99 filled such a value once per call, at the cost of an allocation on every Set that entered a value, and was closed in favor of #103 and #104. A SetDefaults that is not idempotent applies once per path, and a chain of values each shared by two pointers takes time that doubles with every link.Set while another goroutine reads a map of structs it walks. #101 stored a struct back only when it changed, at the cost of an allocation for each struct a tag or setter touched, and was closed in favor of #105.*[]T, *map[K]T or **T holds gets no defaults, and a default that would fail there is not reported, though a *[]T or *map[K]T held as a map value is descended into, as it has been since v1.6.0. #100 descended into all three and was closed in favor of #106, which pins the skip: nobody had asked for the descent, and it would bring new errors and new cycles.#95 pins more behavior no test covered, marking what looks wrong // QUIRK, and corrects documentation that described behavior Set does not have:
default:"0644" is 420, and default:"08080" is an error. Slice and map tags are JSON, where a number rejects a leading zero and an integer map key "010" is 10.{} and [] are never handed to UnmarshalJSON directly, though encoding/json may call it while parsing the other literal.Set, MustSet, CanUpdate, Setter and ErrInvalidType are as in v1.10.0. go.mod still says go 1.22, and CI tests 1.22, 1.26 and 1.27.
Measured with make bench-compare BASE=v1.10.0, the suite from #102: 6 interleaved rounds at 400ms, Go 1.26.5, darwin/arm64 (M1 Max). Every difference shown is significant at p < 0.05. Allocations are unchanged on every row but those named below.
Faster: slices and maps of scalars. Set walked every element of a slice and every entry of a map whatever their type, though only a struct, pointer, slice or map element can hold a default. It now skips the rest (#103). Tagged fields also got cheaper, since the tag is offered to a field's unmarshalers through one interface conversion rather than two (#110):
| v1.10.0 | v1.11.0 | |
|---|---|---|
a filled []int of 1000 |
6031 ns | 55 ns |
a filled map[string]int of 1000 |
54.1 µs, 2001 allocs | 56 ns, 0 allocs |
a map[string]int default parsed from its tag |
937 ns, 17 allocs | 798 ns, 12 allocs |
Set, a struct, a pointer, a slice and a map |
2.27 µs, 32 allocs | 2.07 µs, 29 allocs |
| a struct with one field of every parsed kind | 2.04 µs | 1.93 µs |
ten tagged *int fields |
1.91 µs | 1.79 µs |
Set on four scalar fields is unchanged, at 547 ns against 538 ns (p=0.18).
Slower: walking values that could hold a default, by 3 to 14%. Each struct, slice and map Set enters now checks the path above it for a cycle (#104), and each field it visits takes one more call (#98):
| v1.10.0 | v1.11.0 | |
|---|---|---|
| one untagged struct field | 44.5 ns | 49.1 ns (+10.4%) |
| structs nested by value, 64 deep | 54.9 µs | 62.4 µs (+13.8%) |
| a value eight pointers share | 125 µs | 140 µs (+12.0%) |
| 1000 structs holding a pointer, filled | 204 µs | 221 µs (+8.7%) |
| 100 pointers in a map, filled | 21.8 µs | 23.2 µs (+6.2%) |
| a default that fails four values down | 877 ns | 931 ns (+6.1%) |
Deep data costs a little more, and a few allocations. Down to 64 values deep the cycle check scans the path; below that it builds an index of the path, which is what keeps the cost from growing with the square of the depth (#108). Set on an already-filled linked list of structs, one tagged field each, measured with testing.Benchmark outside the committed suite (the last row is a single call):
| length | v1.10.0 | v1.11.0 |
|---|---|---|
| 10 | 1.4 µs, 0 allocs | 1.9 µs, 0 allocs |
| 100 | 16.7 µs, 0 allocs | 24.8 µs, 3 allocs |
| 1,000 | 179 µs, 0 allocs | 248 µs, 7 allocs |
| 10,000 | 3.07 ms, 0 allocs | 4.53 ms, 15 allocs |
| 100,000 | 54 ms | 68 ms |
A slice of pointers to structs runs in one of two modes. A filled []*T of 1000 elements takes about 187 µs in some processes and about 313 µs in others, where v1.10.0 took 183 µs in every process. A process picks a mode as it starts and keeps it for its whole run. Over twelve fresh processes each, v1.10.0 was fast in twelve, this release's #111 was fast in twelve, and #112 — which only stopped aliasing a struct field to a local — was fast in four. That change removes no work and alters no behavior: the cost is in the machine code the compiler generates around it. The alias stays gone by choice, since keeping a local to steer code generation would argue for hoisting the tag's other fields into locals too. A map of slices moves with it, at 30 µs against 38 µs.
Parse/unmarshaler/text reads 23% faster than on v1.10.0. That is code placement rather than work removed: the same row moved as far with dead code added to an unchanged tree (#108).
Set descends into. (#95, #97, #98, #104, #105, #106)Set's doc comment says what it fills, what it descends into, what it writes to a map and what a failure leaves behind. Setter's says SetDefaults is called once for each path to its struct, but not again from a cycle.MustSet example; the package example calls it. (#95)benchmark_test.go is a suite of 46 cases: whole calls, the zero check, each parse path, walks with nothing left to fill, and failures. make bench-compare BASE=<ref> runs it against another commit in interleaved rounds and compares them with benchstat, and CI runs every case once on each Go version. Every number under Performance but the deep-nesting table comes from it. (#102, #108)Set now live in files of their own, with their tests beside them in package defaults: the promoted-method check (method.go), the unmarshaler handoff (unmarshal.go) and the path check and its index (path.go). They are the only exception to this repository's black-box test rule, since nothing else can reach them. (#109, #110, #111)path.go imports unsafe to hold each value on the path as an unsafe.Pointer, which keeps that value alive so nothing allocated below it can reuse its address, and its index keys hold addresses as uintptr so that entries stay off the heap. There is no pointer arithmetic. (#104, #108)make cover now measures ./... rather than the root package alone. (#109)Run your tests. If a value that a SetDefaults used to adjust comes back as its unmarshaler left it, see the first section. If code carries on after Set returns an error, expect nil and zero values where part of a default used to be. If you match the text of errors from an int64 or time.Duration field, they now name both parsers.
Full Changelog: v1.10.0...v1.11.0
One column per quarter.
Nothing published for this version
v1.9.0 ended with a list of four known issues. This release fixes three of them — #67 , #71 and #79 — and two more bugs, #69 and #89 , and exports the
v1.9.0 ended with a list of four known issues. This release fixes three of them — #67, #71 and #79 — and two more bugs, #69 and #89, and exports the error Set returns for an argument it cannot fill (#70).
Behavior changes come first again. Two of them can change what a working program does. One is loud: a tag that was never applied is now an error. The other is quiet — a SetDefaults that ran only through method promotion no longer runs — so it comes first.
SetDefaults runs once per value, and not through promotion (#85, #86)In v1.9.0 a SetDefaults could run two or three times on the same value. A struct behind a pointer got one call as its fields were filled and another from the pointer (#67). A method promoted from an embedded field ran once for the field and again through the struct embedding it, and once more for each further level of embedding.
A value reached by one path now gets one call, and an embedded field gets exactly the calls a named field of its type would get. For a setter that is idempotent — the usual if c.Port == 0 { c.Port = 8080 } — this first table changes nothing:
SetDefaults calls |
v1.9.0 | v1.10.0 |
|---|---|---|
*T field, tagged default:"{}" or allocated by the caller |
2 | 1 |
element of a []*T |
2 | 1 |
**T field |
2 | 1 |
embedded T |
2 | 1 |
embedded T, two levels deep |
3 | 1 |
embedded *T |
3 | 1 |
That count is per path. A value reached by more than one path — two pointers to one struct, two slices over one array, one map held in two fields — still gets one call from each: two pointers to one struct went from 4 calls to 2, not to 1. See Known issues.
The second table is the one to read twice. These calls happened only through promotion, or after an unmarshaler had already taken the tag, and nothing reports that they are gone:
SetDefaults calls |
v1.9.0 | v1.10.0 |
|---|---|---|
| embedded unexported struct | 1 | 0 |
embedded T tagged default:"-" |
1 | 0 |
embedded interface holding a Setter |
1 | 0 |
embedded T whose UnmarshalText took the tag |
1 | 0 |
*T field, T a struct, whose UnmarshalText took the tag |
1 | 0 |
embedded *T left nil |
called with a nil receiver |
no call |
embedded interface left nil |
panic | no call |
The *T field row is not an embedding. A *T field ran SetDefaults after UnmarshalText where a T field does not; it now agrees with the T field and with the README, which says the tag is handed to UnmarshalText and SetDefaults is not called. That holds only when T is a struct: a pointer to a non-struct type with both methods, such as *Level for type Level int, still gets SetDefaults after UnmarshalText took the tag, as in v1.9.0.
How to tell whether you are affected: look for a type whose SetDefaults you rely on, embedded where it gets no call of its own. A value it used to fill now stays zero:
type base struct{ Timeout time.Duration }
func (b *base) SetDefaults() {
if b.Timeout == 0 {
b.Timeout = 30 * time.Second
}
}
type Client struct {
base // v1.9.0: Timeout is 30s. v1.10.0: Timeout is 0.
}To keep the call, declare SetDefaults on the embedding struct and forward it:
func (c *Client) SetDefaults() { c.base.SetDefaults() }Forward only to a field the second table says gets no call. An exported embedded struct is visited and already gets its call, so forwarding to it runs the setter twice.
Also check any setter that is not idempotent. One that appends, counts or toggles now runs once where it ran two or three times — that is the fix, but it undoes anything that compensated for the repeat.
An embedded non-struct type with a setter, such as type Level int, loses one call too, behind a pointer or not. A struct that declares its own SetDefaults next to an embedded one still gets both calls.
v1.9.0 made an invalid default an error, but missed one path. A type's own UnmarshalText or UnmarshalJSON is offered the tag first, and when it refuses, Set falls back to parsing by kind. Arrays and complex numbers have no parsing by kind, so the refusal went nowhere: the field stayed zero and Set returned nil. For uuid.UUID, a [16]byte, that zero is a nil UUID that looks legitimate. Now:
field ID: invalid default "not-a-uuid": invalid UUID length: 10
| v1.9.0 | v1.10.0 | |
|---|---|---|
uuid.UUID default:"not-a-uuid" |
nil UUID, no error | error |
*uuid.UUID default:"not-a-uuid" |
pointer to a nil UUID, no error | error, pointer still allocated |
a named complex128 whose UnmarshalText rejects the tag |
0, no error |
error |
[3]int default:"[1,2,3]", no unmarshaler |
ignored | ignored |
As with v1.9.0's change, this can stop a program at startup, and only over a tag that never applied. An array type with no unmarshaler has nothing to reject its tag, so the last row is unchanged.
When a type's own unmarshaler refused a tag and parsing by kind failed as well, Set reported the second failure — often from encoding/json, a parser the tag was never written for. It now reports the unmarshaler's:
| v1.9.0 | v1.10.0 | |
|---|---|---|
time.Time default:"garbage" |
invalid character 'g' looking for beginning of value |
parsing time "garbage" as "2006-01-02T15:04:05Z07:00": cannot parse "garbage" as "2006" |
slog.Level default:"bogus" |
strconv.ParseInt: parsing "bogus": invalid syntax |
slog: level string "bogus": unknown name |
a struct wrapping time.Duration with UnmarshalText, default:"garbage" |
invalid character 'g' looking for beginning of value |
time: invalid duration "garbage" |
The field X: invalid default "…": prefix is unchanged and the cause is still wrapped with %w, but errors.As now reaches the unmarshaler's error type — a *time.ParseError for time.Time, where it used to be a *json.SyntaxError.
Nothing that succeeded fails now. The fall-back to parsing by kind stays, so slog.Level with default:"4" — which its unmarshalers reject, since they take names only — is still WARN.
One message gets worse. When both of a type's unmarshalers reject a tag, UnmarshalText's reason is the one reported, since it was asked first. For a JSON-quoted time.Time with a bad date, default:"\"2020-13-01T00:00:00Z\"", that is a complaint about the quote, where v1.9.0 said month out of range.
A type that refers to itself through a field whose tag creates another of it recursed until the stack overflowed. A stack overflow is fatal rather than a panic, so no recover could catch it.
| v1.9.0 | v1.10.0 | |
|---|---|---|
Next *Node default:"{}" |
fatal error: stack overflow |
field Next: default "{}" recurses without end |
Children []Tree default:"[{}]" |
fatal error: stack overflow |
field Children: default "[{}]" recurses without end |
Edges map[string]Graph default:"{\"a\":{}}" |
fatal error: stack overflow |
field Edges: default "{\"a\":{}}" recurses without end |
The check is exact, not a depth limit: it stops where the same tag is about to fill a zero value of the same type inside itself, which is the one case that cannot end. A recursive type that does end — at a field with no tag, a tag that creates nothing, or a value already filled in — is walked as before.
| v1.9.0 | v1.10.0 | |
|---|---|---|
Set(nil) |
panic: runtime error: invalid memory address or nil pointer dereference |
ErrInvalidType |
Set((*Config)(nil)) |
panic: reflect: call of reflect.Value.Type on zero Value |
ErrInvalidType |
MustSet with either |
panics as Set does |
panics with ErrInvalidType |
Code that panicked was never working, so nothing regresses. One consequence: if you ignore Set's error, a nil *Config no longer crashes inside Set, so it crashes later, wherever it is first dereferenced.
ErrInvalidType (#93)The error for an argument that is not a non-nil pointer to a struct is exported, so it can be told apart from a bad tag without matching its text:
if err := defaults.Set(v); errors.Is(err, defaults.ErrInvalidType) {
// v is nil, not a pointer, or not a pointer to a struct
}Its message is still not a struct pointer, so code matching the text keeps working. Test for it with errors.Is rather than ==, as its doc comment says. MustSet panics with the sentinel itself.
Set, MustSet, CanUpdate and Setter are unchanged.
go.mod moves from go 1.21 to go 1.22. The zero-value check now uses reflect.Value.IsZero (#83), which until Go 1.22 compared a float's bits and so read -0.0 as non-zero (golang/go#61827). On 1.21 a float field holding -0.0 would have kept it instead of taking its default; on 1.22 and later it takes the default, as in v1.9.0. CI tests 1.22, 1.26 and 1.27.
Go 1.21 has been out of support since 1.23 shipped. A module still on it can stay on v1.9.0; go get of v1.10.0 raises the module's go line to 1.22.
The zero-value check uses reflect.Value.IsZero rather than reflect.DeepEqual against a fresh zero value (#83), and Set runs it once per field. A struct with a SetDefaults now also pays for the promotion check from #86, which outweighs that saving on a small struct. Measured with make bench plus a struct that has a setter — Go 1.26.5, darwin/arm64, medians of six interleaved runs:
| v1.9.0 | v1.10.0 | |
|---|---|---|
Set, four scalar fields |
711 ns, 9 allocs | 544 ns, 5 allocs |
Set, a struct, a pointer, a slice and a map |
2707 ns, 39 allocs | 2287 ns, 32 allocs |
Set, two fields and a SetDefaults |
384 ns, 6 allocs, 80 B | 474 ns, 6 allocs, 128 B |
CanUpdate, scalar |
25.0 ns | 3.4 ns |
CanUpdate, four-field struct |
98.0 ns, 1 alloc | 26.4 ns, 0 allocs |
example/main.go. The zero-value caveat gets its own section, and the design principles from #61, default:"-", an install line and a pkg.go.dev badge are new. (#82)example/ is replaced by a package example on pkg.go.dev whose output go test checks; the old program's expected output had gone stale. (#91)Setter's doc comment says when SetDefaults is called (#86), and ErrInvalidType's how to test for it (#93).reflect lists a promoted method and a declared one alike, so #86 tells them apart by where the method's code lives: the compiler positions the wrapper it generates for a promoted method in <autogenerated>. The Go spec does not promise that. The suite pins both directions, so if a Go release changes it, the tests fail; CI runs the gc toolchain on 1.22, 1.26 and 1.27.defaults.go is split into set.go, mustset.go and canupdate.go, each beside its tests, and internal/fixture is gone. (#82)test-ok check for a branch rule to require, whatever Go versions the matrix holds. (#88)make bench benchmarks Set and CanUpdate. Statement coverage stays at 100%.time.Duration and a plain int64 share one parser, so default:"1" on a Duration means 1ns and an int64 accepts "1h" (#66). It is the last of v1.9.0's four.SetDefaults once per path, as described above.recover cannot catch.UnmarshalText took the tag still gets SetDefaults afterwards.Set writes back every struct, slice and map value it visits in a map, changed or not, so calling it while another goroutine reads that map is a data race, and can crash with concurrent map read and map write even when there is nothing to fill.*scalar, *[]T or *map field, the pointer stays allocated, so a second Set on the same value returns nil and leaves the zero value behind. The *uuid.UUID row above is the same case.Run your tests. If Set now fails for an array or complex type, that tag was never applied, and the message gives the unmarshaler's reason. If a value that a SetDefaults used to fill comes back zero, find the embedded type it belongs to and forward the call, as shown above. If you inspect errors from a type with its own unmarshaler, the messages and errors.As targets now follow that unmarshaler.
To @dima-starosud, whose review on #64 suggested reflect.Value.IsZero — taken up in #83.
Full Changelog: v1.9.0...v1.10.0
The first release since v1.8.0 (August 2024), and almost entirely other people's work: seven pull requests that had been waiting between eight months
The first release since v1.8.0 (August 2024), and almost entirely other people's
work: seven pull requests that had been waiting between eight months and two years,
adapted onto a rebuilt test suite.
Behavior changes come first, because there are four of them and one is loud. The
exported API is unchanged, so dependent code keeps compiling; what moved is what the
library does with tags that never worked.
A tag that failed to parse used to be discarded silently: the field kept its zero
value and Set returned nil. It now returns an error naming the field, the tag and
the cause.
field Port: invalid default "abc": strconv.ParseInt: parsing "abc": invalid syntax
This is the one to read twice. If you use the idiom from the README —
if err := defaults.Set(&cfg); err != nil {
log.Fatal(err)
}— then a tag that has been quietly broken for years will now stop your program at
startup. That is the point of the change, not a side effect of it.
How to tell whether you are affected: look for a tag that never actually applied.
| v1.8.0 | v1.9.0 | |
|---|---|---|
int default:"abc" |
0, no error |
error |
int8 default:"999" (overflows the type) |
0, no error |
error |
int32 default:"1h" (only int64 takes durations) |
0, no error |
error |
int default:" 1 " (padded number) |
0, no error |
error |
| a bad tag inside a struct reached through a pointer field | swallowed | error (#68) |
Errors also gained the field name and tag as a prefix, so string matching on errors
needs updating even where an error was already returned: a malformed JSON container
tag reported unexpected end of JSON input and now reports
field V: invalid default "[1,2,3": unexpected end of JSON input. The cause is
wrapped with %w, so errors.Is and errors.As reach through to it — *strconv.NumError,
*json.SyntaxError and *json.UnmarshalTypeError are all still matchable.
default:"" now allocates (#63, by @5p2O5pe25ouT)An empty tag used to be indistinguishable from no tag at all. Set reads tags with
StructTag.Lookup now, so default:"" means give me this type's zero value while an
absent tag still means leave this field alone.
| v1.8.0 | v1.9.0 | |
|---|---|---|
*string default:"" |
nil |
pointer to "" |
[]string default:"" |
nil |
empty, non-nil slice |
map[string]int default:"" |
nil |
empty, non-nil map |
Untagged fields are unchanged and still come back nil. The visible consequence is
serialization: null becomes "", [] or {}, so snapshot and golden-file tests
downstream will move.
time.Duration with default:" 10s " produced 0 and now produces 10s.
The trim belongs to the duration attempt alone, which is why a padded number is now
an error rather than silently zero (above). Named duration types are covered too —
type Timeout time.Duration parses " 10s " — because the trim sits in the int64
branch instead of keying off time.Duration's exact type.
CanUpdate(nil) no longer panics (#64, by @lovewave02)It used to panic with reflect: call of reflect.Value.Type on zero Value. It returns
true now. Code that panicked was never working, so nothing can regress here.
go.mod moves from go 1.14 to go 1.21. CI tests the declared floor plus every
currently supported release: 1.21, 1.26 and 1.27. testify is a test-only dependency —
consumers never build it.
Set, MustSet, CanUpdate and Setter are unchanged, and nothing was added or
removed. That is deliberate: this release is behavior and tests, not surface.
*bool is how you let an explicit false survivedefault:"true". Six separate reports had run into this: #60, #49, #37, #31, #30 andencoding.TextUnmarshaler is documented as a second way to set defaults, and it takesdefaults.Setter. (#56, by @llorllale)make cover gate that fails the build below it. Today's behavior isQUIRK orBUG comment with a link, so changing one shows up as a deliberate test diff.shouldInitializeField now reads only thetime.Duration and a plain int64 share one parser, so default:"1" on a Durationint64 accepts "1h". Not cleanly fixable — reflection cannot tellint64 (#66).UnmarshalText or UnmarshalJSON is discarded, so the error you see canSetDefaults is called twice on a pointer-to-struct field (#67).Run your tests. If Set now returns an error, that tag was not working before — the
message names the field and the value. If a nil pointer, slice or map became an empty
one, check whether anything downstream serializes it.
To everyone whose pull request sat unreviewed and is in this release anyway:
@lovewave02, @5p2O5pe25ouT, @boskuv, @fchikwekwe, @marxoffice, @gitsang and
@llorllale.
Full Changelog: v1.8.0...v1.9.0
Nothing published for this version
fix: error when trying initialize slice of structs with default tag
Nothing published for this version
Nothing published for this version
Allow defaults library to use UnmarshalText() and UnmarshalJSON()
Nothing published for this version
Default value to the entry that key already exists in a map
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Set the default value for a pointers (non-struct) when it is nil only Thanks to @mmpereira2github
Add a MustSet function #22 (Thanks to @BoynChan )
Nothing published for this version
Allow to use integer literals #19 (thanks to @licaonfee )
Nothing published for this version
Nothing published for this version
set default values of fields of a pointer type field #18 (Thanks to @sdghchj )
Nothing published for this version
Nothing published for this version
Slice of structs support #9 #10 (thanks to @SpectrumQT )
Propagate JSON unmarshal errors #8
Fix pointer initialization #5 #6
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →