go Version
go version go1.26.5 linux/arm64
API Wrapper Version
github.com/linode/linodego/v2 v2.5.0
Code Snippet
// client.go (since #938)
var (
reqLogTemplate = template.Must(template.New("request").Parse(requestTemplateStr))
respLogTemplate = template.Must(template.New("response").Parse(responseTemplateStr))
)
func (c *Client) logRequest(req *http.Request) *http.Request {
// ...
err := reqLogTemplate.Execute(&logBuf, map[string]any{
// Flat string values only.
})
// ...
}
Expected Behavior
Importing linodego should not disable Go linker optimizations for the consuming binary. The templates only access flat keys such as {{.Request}}, {{.Host}}, and {{.Status}} from a map[string]any containing strings. They do not require reflection-based method lookup, and fmt.Fprintf can produce byte-for-byte identical output.
Actual Behavior
Since #938 removed resty, client.go parses two embedded templates during package initialization and executes them in logRequest and logResponse. The text/template execution engine calls reflect.Value.MethodByName with a non-constant argument, which the compiler marks REFLECTMETHOD.
When this call is statically reachable, the linker retains every exported method of every reachable type instead of pruning unused methods. This behavior is documented for [reflect.Value.MethodByName](https://pkg.go.dev/reflect#Value.MethodByName) and discussed in [golang/go#72895](golang/go#72895).
Runtime logging settings do not affect static reachability, so every binary that constructs a linodego.Client incurs the cost even when debug logging is disabled.
The cost scales with the consumer's dependency tree. The README example pays 17%. In kOps, which links linodego alongside other cloud SDKs, this call site is one of the last two blockers keeping kops-controller at 150.9 MB instead of 69.0 MB, a 54% reduction. Most retained code consists of unused methods from AWS, GCP, Azure SDKs, and client-go.
Steps to Reproduce
-
Save the README "Getting started" example as main.go, then initialize the module:
go mod init repro
go mod tidy
-
Build it and inspect the linker reachability dump:
GOOS=linux GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o repro .
go build -ldflags=-dumpdep -o /dev/null . 2>&1 | grep '<ReflectMethod>'
The dump shows text/template.(*state).evalField <ReflectMethod>, reached through (*Client).logRequest. The binary is 7,667,874 bytes.
-
Replace the two Template.Execute calls in client.go with equivalent fmt.Fprintf calls, point the module to the patched copy with a replace directive, and rebuild.
The <ReflectMethod> symbols disappear, and the example shrinks from 7.6 MB to 6.3 MB. Savings increase with the size of the consumer's dependency tree.
go Version
go version go1.26.5 linux/arm64
API Wrapper Version
github.com/linode/linodego/v2 v2.5.0
Code Snippet
Expected Behavior
Importing
linodegoshould not disable Go linker optimizations for the consuming binary. The templates only access flat keys such as{{.Request}},{{.Host}}, and{{.Status}}from amap[string]anycontaining strings. They do not require reflection-based method lookup, andfmt.Fprintfcan produce byte-for-byte identical output.Actual Behavior
Since #938 removed resty,
client.goparses two embedded templates during package initialization and executes them inlogRequestandlogResponse. Thetext/templateexecution engine callsreflect.Value.MethodByNamewith a non-constant argument, which the compiler marksREFLECTMETHOD.When this call is statically reachable, the linker retains every exported method of every reachable type instead of pruning unused methods. This behavior is documented for
[reflect.Value.MethodByName](https://pkg.go.dev/reflect#Value.MethodByName)and discussed in [golang/go#72895](golang/go#72895).Runtime logging settings do not affect static reachability, so every binary that constructs a
linodego.Clientincurs the cost even when debug logging is disabled.The cost scales with the consumer's dependency tree. The README example pays 17%. In kOps, which links
linodegoalongside other cloud SDKs, this call site is one of the last two blockers keepingkops-controllerat 150.9 MB instead of 69.0 MB, a 54% reduction. Most retained code consists of unused methods from AWS, GCP, Azure SDKs, andclient-go.Steps to Reproduce
Save the README "Getting started" example as
main.go, then initialize the module:Build it and inspect the linker reachability dump:
The dump shows
text/template.(*state).evalField <ReflectMethod>, reached through(*Client).logRequest. The binary is 7,667,874 bytes.Replace the two
Template.Executecalls inclient.gowith equivalentfmt.Fprintfcalls, point the module to the patched copy with areplacedirective, and rebuild.The
<ReflectMethod>symbols disappear, and the example shrinks from 7.6 MB to 6.3 MB. Savings increase with the size of the consumer's dependency tree.