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.
[BREAKING] An invalid default value is now an error (#59, by @marxoffice)
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.
[BREAKING] 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.
A padded duration tag now parses (#55, by @boskuv)
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.
Minimum Go version
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.
No API change
Set, MustSet, CanUpdate and Setter are unchanged, and nothing was added or
removed. That is deliberate: this release is behavior and tests, not surface.
Documentation
- The README now says what a zero value can and cannot express, and that a pointer is
how you keep the distinction —*boolis how you let an explicitfalsesurvive
default:"true". Six separate reports had run into this: #60, #49, #37, #31, #30 and
#15. (#51, by @fchikwekwe) encoding.TextUnmarshaleris documented as a second way to set defaults, and it takes
precedence overdefaults.Setter. (#56, by @llorllale)
Internals
- The test suite was rewritten. One 771-line file built around a single ~120-field
struct became 15 black-box files exercising only the public API, at 100% statement
coverage with amake covergate that fails the build below it. Today's behavior is
pinned test by test, including the parts that look wrong — those carry aQUIRKor
BUGcomment with a link, so changing one shows up as a deliberate test diff.
(#65, #72) - CI moved to GitHub Actions from a CircleCI config that had stopped running.
(#65) - The map element loop was flattened, and
shouldInitializeFieldnow reads only the
field with the tag hoisted to its caller. Neither changes behavior. (#58, by @gitsang)
Known issues, unchanged by this release
time.Durationand a plainint64share one parser, sodefault:"1"on a Duration
means 1ns and anint64accepts"1h". Not cleanly fixable — reflection cannot tell
a named duration type from a namedint64(#66).- A failing
UnmarshalTextorUnmarshalJSONis discarded, so the error you see can
name the wrong parser (#79). SetDefaultsis called twice on a pointer-to-struct field (#67).- A self-referential type whose tag creates an element recurses until the stack
overflows (#71).
Upgrading
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.
Thanks
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