Accepted proposal: range over all command-line flags
Go’s proposal for package flag was accepted on July 23, 2026. It fixes an awkward case in programs that read the same setting from command-line flags, environment variables, and defaults.
Suppose a server has a -debug flag whose default is false:
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
debug := fs.Bool("debug", false, "enable debug logging")
if err := fs.Parse(args); err != nil {
return err
}The server also reads APP_DEBUG. A command-line flag should win over the environment
variable, and the environment variable should win over the default. If the user runs the
server with -debug=false while APP_DEBUG=true, the server must keep debug mode off. But
*debug is false both when the flag was omitted and when the user explicitly set it to
false. The parsed value alone cannot distinguish those cases.
The flag package knows which flags were set, but today it exposes that information through
two callbacks. Visit walks only the flags that were set successfully. VisitAll walks
every declared flag. Code that fills unset flags from the environment has to combine both:
func applyEnv(fs *flag.FlagSet) error {
alreadySet := make(map[string]bool)
fs.Visit(func(f *flag.Flag) {
alreadySet[f.Name] = true
})
var applyErr error
fs.VisitAll(func(f *flag.Flag) {
if applyErr != nil || alreadySet[f.Name] {
return
}
key := "APP_" + strings.ToUpper(
strings.ReplaceAll(f.Name, "-", "_"),
)
value, ok := os.LookupEnv(key)
if !ok {
return
}
if err := fs.Set(f.Name, value); err != nil {
applyErr = fmt.Errorf(
"set flag -%s from %s: %w", f.Name, key, err,
)
}
})
return applyErr
}The first pass records the flags found by Visit. The second pass visits everything and
looks up an environment variable only when the name is absent from that map. applyErr sits
outside the callback because the callback cannot return an error from applyEnv. The flagx
environment-variable helper follows this two-pass approach.
The proposal adds the set state to each Flag and provides an iterator over all flags:
type Flag struct {
// existing fields
IsSet bool
}
func All() iter.Seq[*Flag]
func (*FlagSet) All() iter.Seq[*Flag]The same helper becomes one loop:
func applyEnv(fs *flag.FlagSet) error {
for f := range fs.All() {
if f.IsSet {
continue
}
key := "APP_" + strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
value, ok := os.LookupEnv(key)
if !ok {
continue
}
if err := fs.Set(f.Name, value); err != nil {
return fmt.Errorf("set flag -%s from %s: %w", f.Name, key, err)
}
}
return nil
}fs.All() yields every flag in name order. f.IsSet says whether parsing or a call to
fs.Set successfully assigned that flag. An explicit -debug=false therefore has
IsSet == true, so APP_DEBUG cannot overwrite it. If -debug was omitted, the loop reads
APP_DEBUG and passes its text through the flag’s normal parser. With neither source
present, the original default remains.
The side map, the first traversal, and the callback error variable disappear. The loop can
return an error directly. flag.All() provides the same iteration for the package-level
flag.CommandLine.
The implementation has passed the Go trybots and is still under review. It has not landed on the main branch or appeared in a tagged release yet.