encoding/json/v2: avoid escaping jsonopts.Struct

The jsonopts.Struct.join method unfortunately escapes
the receiver because it is passed to JoinUnknownOption,
which is a dynamically implemented function.

This affects jsontext.Encoder.reset and jsontext.Decoder.reset,
which relied on a local jsonopts.Struct to temporarily store
prior options such that it would have to be heap allocated.

Adjust the signature of JoinUnknownOption to avoid pointers
so that nothing escape.

This is a regression from
https://github.com/go-json-experiment/json/pull/163

Performance:

	name             old time/op    new time/op    delta
	Marshal/Bool-32    72.1ns ± 2%    51.3ns ± 1%  -28.77%  (p=0.000 n=10+9)

	name             old allocs/op  new allocs/op  delta
	Marshal/Bool-32      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.000 n=10+10)

Updates #71845

Change-Id: Ife500d82d3d2beb13652553a4ffdf882c136f5a0
Reviewed-on: https://go-review.googlesource.com/c/go/+/685135
Auto-Submit: Joseph Tsai <joetsai@digital-static.net>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
This commit is contained in:
Joe Tsai
2025-06-30 15:40:20 -07:00
committed by Gopher Robot
parent e46d586edd
commit 6bd9944c9a
2 changed files with 7 additions and 6 deletions

View File

@@ -65,7 +65,7 @@ func (*Struct) JSONOptions(internal.NotForPublicUse) {}
// GetUnknownOption is injected by the "json" package to handle Options
// declared in that package so that "jsonopts" can handle them.
var GetUnknownOption = func(*Struct, Options) (any, bool) { panic("unknown option") }
var GetUnknownOption = func(Struct, Options) (any, bool) { panic("unknown option") }
func GetOption[T any](opts Options, setter func(T) Options) (T, bool) {
// Collapse the options to *Struct to simplify lookup.
@@ -104,14 +104,14 @@ func GetOption[T any](opts Options, setter func(T) Options) (T, bool) {
}
return any(structOpts.DepthLimit).(T), true
default:
v, ok := GetUnknownOption(structOpts, opt)
v, ok := GetUnknownOption(*structOpts, opt)
return v.(T), ok
}
}
// JoinUnknownOption is injected by the "json" package to handle Options
// declared in that package so that "jsonopts" can handle them.
var JoinUnknownOption = func(*Struct, Options) { panic("unknown option") }
var JoinUnknownOption = func(Struct, Options) Struct { panic("unknown option") }
func (dst *Struct) Join(srcs ...Options) {
dst.join(false, srcs...)
@@ -182,7 +182,7 @@ func (dst *Struct) join(excludeCoderOptions bool, srcs ...Options) {
}
}
default:
JoinUnknownOption(dst, src)
*dst = JoinUnknownOption(*dst, src)
}
}
}

View File

@@ -257,7 +257,7 @@ func (*unmarshalersOption) JSONOptions(internal.NotForPublicUse) {}
// Inject support into "jsonopts" to handle these types.
func init() {
jsonopts.GetUnknownOption = func(src *jsonopts.Struct, zero jsonopts.Options) (any, bool) {
jsonopts.GetUnknownOption = func(src jsonopts.Struct, zero jsonopts.Options) (any, bool) {
switch zero.(type) {
case *marshalersOption:
if !src.Flags.Has(jsonflags.Marshalers) {
@@ -273,7 +273,7 @@ func init() {
panic(fmt.Sprintf("unknown option %T", zero))
}
}
jsonopts.JoinUnknownOption = func(dst *jsonopts.Struct, src jsonopts.Options) {
jsonopts.JoinUnknownOption = func(dst jsonopts.Struct, src jsonopts.Options) jsonopts.Struct {
switch src := src.(type) {
case *marshalersOption:
dst.Flags.Set(jsonflags.Marshalers | 1)
@@ -284,5 +284,6 @@ func init() {
default:
panic(fmt.Sprintf("unknown option %T", src))
}
return dst
}
}