You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
urfave/cli directly uses text/template, and unfortunately this partially disables dead code elimination. However, "partial" in this context can result in significantly larger binaries depending on how many other dependencies you have. Full DCE can result in significant binary size reductions (50%+-).
Cobra recently added a fix/workaround for a similar issue: spf13/cobra#1956
I appreciate there may not be a non-breaking way to solve this in v3.
Reacted by Luis Schulte, Gabe Cook, gcooktns and Ludovic Fernandez
The Datadog fork is a drop in replacement for stdlib, with one major difference, it does not use reflect.MethodByName to avoid disabling DCE. Datadog have run into the exact same problem: https://www.datadoghq.com/blog/engineering/agent-go-binaries/
This means you cannot call methods from within templates, FullName(), VisibleFlags() etc. .
I copied the necessary data from the Command into another struct, with no methods, and pass that as the template data.
Ideally yes. Currently the discussions there revolve around:
Introducing a new template function, ExecuteLite, that would not do reflection.
A new build tag to disable reflection in templating (runtime breakage likely).
Try do some compiler wizardry with templating to keep DCE but ensure template method calls still work.
I speculate 1 is most likely since it is the most practical and backwards compatible way to go about it. That said, any change on the stdlib/compiler level is likely to be stuck in limbo for a long time, probably years, while a large part of the Go ecosystem suffers from DCE being disabled with common imports.
This is a well-known Go binary size issue. The text/template and html/template packages import a lot of functionality for safety, which pulls in significant code.
It's not the size of the packages, but the use of reflect.MethodByName in those packages which is the issue. This disables method dead code elimination. This means instead of only used methods being compiled into your binary, all methods are. This is especially bad if you have quite a few dependencies.
If you use reflect.MethodByName directly in your code without using text/template or html/template, you get the same result. text/template and html/template are just a common and unexpected way that reflect.MethodByName usage finds its way into your code.
For a workaround in the meantime, you can use ldflags to strip unused code during build:
go build -ldflags="-s -w"
The -s flag removes the symbol table and -w removes DWARF debugging info. This typically reduces binary size by 20-30%.
It's a good tip in general for binary size reduction, but my example with one of our binaries shrinking from 33mb to 8mb was already using the -s -w ldflags.
The problem is related to DefaultPrintHelpCustom because of the usage of text/template.
But in fact DefaultPrintHelpCustom is problematic because it is used by the global variable HelpPrinterCustom.
A global variable cannot be "bypassed" by the lib user even with an init function.
The global variables are always initiated first (global variables are 99% of time a problem inside a lib).
If you modify the global variable HelpPrinterCustom, inside urfave/cli, with the following code, the problem is gone.
This change is, obviously breaking, and doesn't offer alternative by default for generating the help.
A user should set up the global var with DefaultPrintHelpCustom, but it this case the DCE problem is coming back.
We can think to a way to provide the help statically, but the current signature of HelpPrinterCustomFunc is too "low level" to be used for that.
The problem is not simple (IMHO impossible) to solve without breaking changes because of the usage of a global variable.
@ldez I agree, I don't see a way to fix this without a breaking change.
Even with my approach, which doesn't change the public surface area of the lib, it would still be a behavior breaking change.
I think there is maybe a way by using a build tag with my idea: using a build tag notemplate to isolate the global variable HelpPrinterCustom can work.
Based on that you can use your approach inside your project and creating a custom HelpPrinterCustomFunc.
This is non-breaking, but this is not really a proper way to handle the problem, this is more a temporarily fix before a v4.
🤔 still with a build tag notemplate where HelpPrinterCustom is an empty function: the global variables ShowCommandHelp and ShowRootCommandHelp can be overridden.
These 2 global variables are functions:
ShowRootCommandHelp is dedicated for the root command, so it can be replace with static help
ShowCommandHelp takes the parent command and the name of the current command, so it also can be replaced with static help
So this approach can work by using your approach based on a fork of text/template inside your project and with my idea of generating static content for the help.
Activity
seanamos commentedon Mar 11, 2026
See also the relevant discussion here: golang/go#72895
seanamos commentedon Mar 12, 2026
I did a quick experiment (really hacked together) to demonstrate how significant this is.
In the empty hello world example, the binary size dropped to 1.3mb from 2.0mb
Usin my local fork in a more real world project with a binary size of 33mb, the size dropped to 8mb.
The approach I used? Swap out the usage of
text/templatefor Datadog's fork: https://github.com/DataDog/datadog-agent/tree/main/pkg/template/textThe Datadog fork is a drop in replacement for stdlib, with one major difference, it does not use
reflect.MethodByNameto avoid disabling DCE. Datadog have run into the exact same problem: https://www.datadoghq.com/blog/engineering/agent-go-binaries/This means you cannot call methods from within templates,
FullName(),VisibleFlags()etc. .I copied the necessary data from the
Commandinto another struct, with no methods, and pass that as the template data.There are some other changes, but that was the general idea.
abitrolly commentedon Mar 22, 2026
It looks like this should be in Go
stdlib.abitrolly commentedon Mar 22, 2026
Could this help with eliminating completions code that is not used? #2291
seanamos commentedon Mar 26, 2026
Ideally yes. Currently the discussions there revolve around:
ExecuteLite, that would not do reflection.I speculate 1 is most likely since it is the most practical and backwards compatible way to go about it. That said, any change on the stdlib/compiler level is likely to be stuck in limbo for a long time, probably years, while a large part of the Go ecosystem suffers from DCE being disabled with common imports.
abitrolly commentedon Mar 27, 2026
I would just call it
Render.ExecuteLitedoesn't really show what does it do. If a breakage is expected, maybe it is possible to introduce a stripped down alternative from most popular libs like https://github.com/mitsuhiko/minijinja/tree/main/minijinja-goEDIT: Link to Go project.
seanamos commentedon Mar 31, 2026
It's not the size of the packages, but the use of
reflect.MethodByNamein those packages which is the issue. This disables method dead code elimination. This means instead of only used methods being compiled into your binary, all methods are. This is especially bad if you have quite a few dependencies.If you use
reflect.MethodByNamedirectly in your code without usingtext/templateorhtml/template, you get the same result.text/templateandhtml/templateare just a common and unexpected way thatreflect.MethodByNameusage finds its way into your code.It's a good tip in general for binary size reduction, but my example with one of our binaries shrinking from 33mb to 8mb was already using the
-s -wldflags.ldez commentedon Apr 17, 2026
We can think the problem differently.
The problem is related to
DefaultPrintHelpCustombecause of the usage oftext/template.But in fact
DefaultPrintHelpCustomis problematic because it is used by the global variableHelpPrinterCustom.A global variable cannot be "bypassed" by the lib user even with an
initfunction.The global variables are always initiated first (global variables are 99% of time a problem inside a lib).
If you modify the global variable
HelpPrinterCustom, insideurfave/cli, with the following code, the problem is gone.This change is, obviously breaking, and doesn't offer alternative by default for generating the help.
A user should set up the global var with
DefaultPrintHelpCustom, but it this case the DCE problem is coming back.We can think to a way to provide the help statically, but the current signature of
HelpPrinterCustomFuncis too "low level" to be used for that.The problem is not simple (IMHO impossible) to solve without breaking changes because of the usage of a global variable.
seanamos commentedon Apr 21, 2026
@ldez I agree, I don't see a way to fix this without a breaking change.
Even with my approach, which doesn't change the public surface area of the lib, it would still be a behavior breaking change.
ldez commentedon Apr 21, 2026
I think there is maybe a way by using a build tag with my idea: using a build tag
notemplateto isolate the global variableHelpPrinterCustomcan work.Based on that you can use your approach inside your project and creating a custom
HelpPrinterCustomFunc.This is non-breaking, but this is not really a proper way to handle the problem, this is more a temporarily fix before a v4.
ldez commentedon Apr 21, 2026
🤔 still with a build tag
notemplatewhereHelpPrinterCustomis an empty function: the global variablesShowCommandHelpandShowRootCommandHelpcan be overridden.These 2 global variables are functions:
ShowRootCommandHelpis dedicated for the root command, so it can be replace with static helpShowCommandHelptakes the parent command and the name of the current command, so it also can be replaced with static helpSo this approach can work by using your approach based on a fork of
text/templateinside your project and with my idea of generating static content for the help.WDYT?
ldez commentedon Apr 23, 2026
Edit: we can ignore it, the problem was related to something else, so my proposal based on build tag still works.
Sadly, there is another usage oftext/templateinsidefish.goThe solution can still works but the fish completion must be rewritten without a template or with a lax approach.