Go Flag Library: A Complete Guide to CLI Arguments
Daniel Hayes
Full-Stack Engineer · Leapcell

Introduction
flag
is used to parse command-line options. People with experience using Unix-like systems should be familiar with command-line options. For example, the command ls -al
lists detailed information about all files and directories in the current directory, where -al
is the command-line option.
Command-line options are commonly used in actual development, especially when writing tools.
Specify the path of the configuration file. For example, postgres -D /usr/local/pgsql/data
starts the PostgreSQL server with the specified data directory;
Customize certain parameters. For example, python -m SimpleHTTPServer 8080
starts an HTTP server listening on port 8080. If not specified, it will listen on port 8000 by default.
Quick Start
The first step in learning a library is of course to use it. Let's first take a look at the basic usage of the flag
library:
package main import ( "fmt" "flag" ) var ( intflag int boolflag bool stringflag string ) func init() { flag.IntVar(&intflag, "intflag", 0, "int flag value") flag.BoolVar(&boolflag, "boolflag", false, "bool flag value") flag.StringVar(&stringflag, "stringflag", "default", "string flag value") } func main() { flag.Parse() fmt.Println("int flag:", intflag) fmt.Println("bool flag:", boolflag) fmt.Println("string flag:", stringflag) }
You can first compile the program and then run it (I'm using macOS):
$ go build -o main main.go $ ./main -intflag 12 -boolflag 1 -stringflag test
Output:
int flag: 12
bool flag: true
string flag: test
If you don't set a certain option, the corresponding variable will take the default value:
$ ./main -intflag 12 -boolflag 1
Output:
int flag: 12
bool flag: true
string flag: default
You can see that the option stringflag
that was not set has the default value default
.
You can also directly use go run
. This command will first compile the program to generate an executable file and then execute the file, passing other options in the command line to this program.
$ go run main.go -intflag 12 -boolflag 1
You can use -h
to display the option help information:
$ ./main -h Usage of /path/to/main: -boolflag bool flag value -intflag int int flag value -stringflag string string flag value (default "default")
To summarize, the general steps to use the flag
library:
- Define some global variables to store the values of the options, such as
intflag
,boolflag
, andstringflag
here; - Use the
flag.TypeVar
method in theinit
method to define the options. Here,Type
can be a basic type likeInt
,Uint
,Float64
,Bool
, or it can also be a time intervaltime.Duration
. When defining, pass in the address of the variable, the option name, the default value, and the help information; - Call
flag.Parse
in themain
method to parse the options fromos.Args[1:]
. Sinceos.Args[0]
is the path of the executable program, it will be excluded.
Points to Note
The flag.Parse
method must be called after all options are defined, and no new options can be defined after flag.Parse
is called. If you follow the previous steps, there will basically be no problems.
Since init
is executed before all the code, by putting all the option definitions in init
, all options will already be defined when flag.Parse
is executed in the main
function.
Option Format
The flag
library supports three command-line option formats.
-flag
-flag=x
-flag x
Both -
and --
can be used, and they have the same function. Some libraries use -
to represent short options and --
to represent long options. Relatively speaking, flag
is easier to use.
The first form only supports boolean options. If it appears, it is true
, and if it does not appear, it takes the default value.
The third form does not support boolean options. Because boolean options in this form may exhibit unexpected behavior in Unix-like systems. Consider the following command:
cmd -x *
Here, *
is a shell wildcard. If there are files named 0
or false
, the boolean option -x
will take the value false
. Otherwise, the boolean option -x
will take the value true
. And this option consumes one argument.
If you want to explicitly set a boolean option to false
, you can only use the form -flag=false
.
Parsing stops when the first non-option argument (that is, an argument that does not start with -
or --
) or the terminator --
is encountered. Run the following program:
$ ./main noflag -intflag 12
The output will be:
int flag: 0
bool flag: false
string flag: default
Because the parsing stops when it encounters noflag
, and the subsequent option -intflag
is not parsed. So all options take their default values.
Run the following program:
$ ./main -intflag 12 -- -boolflag=true
The output will be:
int flag: 12
bool flag: false
string flag: default
First, the option intflag
is parsed and its value is set to 12. After encountering --
, the parsing stops, and the subsequent --boolflag=true
is not parsed, so the boolflag
option takes the default value false
.
After the parsing stops, if there are still command-line arguments, the flag
library will store them, and you can get a slice of these arguments through the flag.Args
method.
You can get the number of unparsed arguments through the flag.NArg
method, and access the argument at position i
(starting from 0) through flag.Arg(i)
.
The number of options can also be obtained by calling the flag.NFlag
method.
Slightly modify the above program:
func main() { flag.Parse() fmt.Println(flag.Args()) fmt.Println("Non-Flag Argument Count:", flag.NArg()) for i := 0; i < flag.NArg(); i++ { fmt.Printf("Argument %d: %s\n", i, flag.Arg(i)) } fmt.Println("Flag Count:", flag.NFlag()) }
Compile and run this program:
$ go build -o main main.go $ ./main -intflag 12 -- -stringflag test
Output:
[-stringflag test]
Non-Flag Argument Count: 2
Argument 0: -stringflag
Argument 1: test
After the parsing stops when it encounters --
, the remaining arguments -stringflag test
are saved in flag
, and you can access them through methods like Args
, NArg
, and Arg
.
Integer option values can accept forms like 1234
(decimal), 0664
(octal), and 0x1234
(hexadecimal), and can also be negative. In fact, flag
internally uses the strconv.ParseInt
method to parse the string into an int
.
So theoretically, any format accepted by ParseInt
is okay.
Boolean option values can be:
- Values for
true
:1
,t
,T
,true
,TRUE
,True
; - Values for
false
:0
,f
,F
,false
,FALSE
,False
.
Another Way to Define Options
Above, we introduced using flag.TypeVar
to define options. This method requires us to first define the variable and then pass in the address of the variable.
There is another way. Calling flag.Type
(where Type
can be Int
, Uint
, Bool
, Float64
, String
, Duration
, etc.) will automatically allocate a variable for us and return the address of that variable. The usage is similar to the previous way:
package main import ( "fmt" "flag" ) var ( intflag *int boolflag *bool stringflag *string ) func init() { intflag = flag.Int("intflag", 0, "int flag value") boolflag = flag.Bool("boolflag", false, "bool flag value") stringflag = flag.String("stringflag", "default", "string flag value") } func main() { flag.Parse() fmt.Println("int flag:", *intflag) fmt.Println("bool flag:", *boolflag) fmt.Println("string flag:", *stringflag) }
Compile and run the program:
$ go build -o main main.go $ ./main -intflag 12
The output will be:
int flag: 12
bool flag: false
string flag: default
Except that dereferencing is required when using it, it is basically the same as the previous method.
Advanced Usage
Defining Short Options
The flag
library does not explicitly support short options, but it can be achieved by setting different options for the same variable. That is, two options share the same variable.
Since the initialization order is uncertain, it is necessary to ensure that they have the same default value. Otherwise, when this option is not passed in, the behavior is uncertain.
package main import ( "fmt" "flag" ) var logLevel string func init() { const ( defaultLogLevel = "DEBUG" usage = "set log level value" ) flag.StringVar(&logLevel, "log_type", defaultLogLevel, usage) flag.StringVar(&logLevel, "l", defaultLogLevel, usage + "(shorthand)") } func main() { flag.Parse() fmt.Println("log level:", logLevel) }
Compile and run the program:
$ go build -o main main.go $ ./main -log_type WARNING $ ./main -l WARNING
Using both long and short options will output:
log level: WARNING
If you don't pass in this option, it will output the default value:
$ ./main log level: DEBUG
Parsing Time Intervals
In addition to using basic types as options, the flag
library also supports the time.Duration
type, that is, time intervals. The formats supported for time intervals are very diverse, such as "300ms"
, "-1.5h"
, "2h45m"
, and so on.
The time units can be ns
, us
, ms
, s
, m
, h
, day
, etc. In fact, flag
will internally call time.ParseDuration
. The specific supported formats can be found in the documentation of the time
library.
package main import ( "flag" "fmt" "time" ) var ( period time.Duration ) func init() { flag.DurationVar(&period, "period", 1*time.Second, "sleep period") } func main() { flag.Parse() fmt.Printf("Sleeping for %v...", period) time.Sleep(period) fmt.Println() }
According to the passed command-line option period
, the program will sleep for the corresponding time, with a default of 1 second. Compile and run the program:
$ go build -o main main.go $ ./main Sleeping for 1s... $ ./main -period 1m30s Sleeping for 1m30s...
Customizing Options
In addition to using the option types provided by the flag
library, we can also customize option types. Let's analyze the example provided in the standard library:
package main import ( "errors" "flag" "fmt" "strings" "time" ) type interval []time.Duration func (i *interval) String() string { return fmt.Sprint(*i) } func (i *interval) Set(value string) error { if len(*i) > 0 { return errors.New("interval flag already set") } for _, dt := range strings.Split(value, ",") { duration, err := time.ParseDuration(dt) if err != nil { return err } *i = append(*i, duration) } return nil } var ( intervalFlag interval ) func init() { flag.Var(&intervalFlag, "deltaT", "comma-seperated list of intervals to use between events") } func main() { flag.Parse() fmt.Println(intervalFlag) }
First, define a new type. Here, the type interval
is defined.
The new type must implement the flag.Value
interface:
// src/flag/flag.go type Value interface { String() string Set(string) error }
The String
method formats the value of this type, and when the flag.Parse
method is executed and encounters an option of the custom type, it will call the Set
method of the variable of this type with the option value as the parameter.
Here, the time intervals separated by ,
are parsed and stored in a slice.
The definition of custom type options must use the flag.Var
method.
Compile and execute the program:
$ go build -o main main.go $ ./main -deltaT 30s [30s] $ ./main -deltaT 30s,1m,1m30s [30s 1m0s 1m30s]
If the specified option value is illegal, the Set
method returns a value of type error
, the execution of Parse
stops, and the error and usage help are printed.
$ ./main -deltaT 30x invalid value "30x" for flag -deltaT: time: unknown unit x in duration 30x Usage of /path/to/main: -deltaT value comma-seperated list of intervals to use between events
Parsing Strings in the Program
Sometimes options are not passed through the command line. For example, they are read from a configuration table or generated by the program. In this case, you can use the relevant methods of the flag.FlagSet
structure to parse these options.
In fact, the methods of the flag
library we called earlier will all indirectly call the methods of the FlagSet
structure. The flag
library defines a global variable CommandLine
of type FlagSet
specifically for parsing command-line options.
The methods of the flag
library we called earlier are just for convenience, and internally they all call the corresponding methods of CommandLine
:
// src/flag/flag.go var CommandLine = NewFlagSet(os.Args[0], ExitOnError) func Parse() { CommandLine.Parse(os.Args[1:]) } func IntVar(p *int, name string, value int, usage string) { CommandLine.Var(newIntValue(value, p), name, usage) } func Int(name string, value int, usage string) *int { return CommandLine.Int(name, value, usage) } func NFlag() int { return len(CommandLine.actual) } func Arg(i int) string { return CommandLine.Arg(i) } func NArg() int { return len(CommandLine.args) }
Similarly, we can also create our own variables of type FlagSet
to parse options.
package main import ( "flag" "fmt" ) func main() { args := []string{"-intflag", "12", "-stringflag", "test"} var intflag int var boolflag bool var stringflag string fs := flag.NewFlagSet("MyFlagSet", flag.ContinueOnError) fs.IntVar(&intflag, "intflag", 0, "int flag value") fs.BoolVar(&boolflag, "boolflag", false, "bool flag value") fs.StringVar(&stringflag, "stringflag", "default", "string flag value") fs.Parse(args) fmt.Println("int flag:", intflag) fmt.Println("bool flag:", boolflag) fmt.Println("string flag:", stringflag) }
The NewFlagSet
method has two parameters. The first parameter is the program name, which will be displayed when outputting help or when an error occurs. The second parameter is how to handle errors during parsing, and there are several options:
ContinueOnError
: Continue parsing after an error occurs.CommandLine
uses this option;ExitOnError
: Callos.Exit(2)
to exit the program when an error occurs;PanicOnError
: Generate apanic
when an error occurs.
Take a quick look at the relevant code in the flag
library:
// src/flag/flag.go func (f *FlagSet) Parse(arguments []string) error { f.parsed = true f.args = arguments for { seen, err := f.parseOne() if seen { continue } if err == nil { break } switch f.errorHandling { case ContinueOnError: return err case ExitOnError: os.Exit(2) case PanicOnError: panic(err) } } return nil }
It's a bit different from directly using the methods of the flag
library. When the FlagSet
calls the Parse
method, it needs to explicitly pass in a slice of strings as a parameter. Because flag.Parse
internally calls CommandLine.Parse(os.Args[1:])
.
Leapcell: The Next-Gen Serverless Platform for Web Hosting
Finally, I'd like to recommend a platform that is most suitable for deploying Go services: Leapcell
1. Multi-Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ