How Go Handles Command‑Line Arguments: From C Basics to Runtime Internals
This article explains how Go executables receive and process command‑line arguments and environment variables, compares C and Go implementations, dives into the Linux stack layout, and details the flag package's parsing mechanics with code examples and runtime internals.
In most cases a useful executable reads data from the command line at startup, using those arguments to control its runtime state, input, output, and more; Go compiled binaries are no exception.
Environment
Declaration
All analysis and description are based on a Linux/amd64 environment.
During memory analysis, address values may differ due to OS, architecture, language, program size, or number of parameters; do not focus on exact addresses.
Code Listings
C Command‑Line Arguments
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
printf("argc=%d, argv=%p, envp=%p
", argc, argv, envp);
// show command line args
char **arg = argv;
while (*arg != NULL)
{
printf("Arg >> %s
", *arg);
arg++;
}
printf("
");
// show environments
char **env = envp;
while (*env != NULL)
{
printf("Env >> %s
", *env);
env++;
}
return 0;
}Go Command‑Line Arguments
// command_line_args.go
package main
import (
"flag"
"fmt"
)
var name = flag.String("name", "world", "user name")
var age int
func init() {
flag.IntVar(&age, "age", 0, "user age")
}
func main() {
flag.Parse()
fmt.Printf("hello %s, age is %d
", *name, age)
fmt.Printf("remaining args: %v
", flag.Args())
}Executable Startup Parameters
On Linux, the kernel allocates a separate virtual memory space for each executable, loads code and data, creates heap and stack, and stores environment variables, command‑line arguments, and other data on the stack.
Setting a breakpoint at the program entry (_start) after compiling the C example lets us inspect the stack and see that four arguments (argc=4) are present: the program name plus three user‑provided arguments.
Memory layout observations:
At address 0x7fffffffdf40 (stack top) the 8‑byte value stores argc.
At 0x7fffffffdf48 (stack top+8) the argv pointer resides; the following 32 bytes hold four pointers to the argument strings.
At 0x7fffffffdf68 the 8‑byte value 0x0 marks the end of the argument list (NULL).
At 0x7fffffffdf70 the envp pointer resides; following memory contains pointers to all environment strings, terminated by a NULL.
The same mechanism applies to Go executables; after compiling the Go example and breaking at _rt0_amd64_linux, we observe:
0x7fffffffdf30 stores argc.
0x7fffffffdf38 holds argv; four argument strings are shown in red.
0x7fffffffdf60 holds envp; Go reads environment variables from this region but does not expose an envp symbol, using a nil pointer to separate arguments from environment variables.
Golang Command‑Line Argument Storage
In Linux, the entry point for Go executables is the function _rt0_amd64_linux defined in runtime/rt0_linux_amd64.s, which simply jumps to _rt0_amd64.
The _rt0_amd64 function, defined in runtime/asm_amd64.s, performs three actions:
Copies argc from the stack into the RDI register.
Copies argv from the stack into the RSI register.
Jumps to runtime.rt0_go to continue execution.
Generally, ordinary Go functions follow the “stack‑based parameter and return value” calling convention; system calls follow the Linux convention using RAX for the syscall number and RDI, RSI, RDX, R10, R8, R9 for parameters.
The runtime.rt0_go function completes all runtime initialization, including saving command‑line arguments and environment variables, and never returns.
1. Saving argc and argv
In runtime/asm_amd64.s, runtime.rt0_go calls runtime.args with argc and argv:
TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0
......
MOVL 16(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
......The runtime.args function (in runtime/runtime1.go) stores these values in global variables:
var (
argc int32
argv **byte
)
func args(c int32, v **byte) {
argc = c
argv = v
sysargs(c, v)
}In C terms this is equivalent to:
int runtime_argc;
char **runtime_argv;
int main(int argc, char **argv) {
runtime_argc = argc;
runtime_argv = argv;
return 0;
}2. Converting argc/argv to Go strings
After runtime.args, runtime.rt0_go calls runtime.schedinit, which invokes runtime.goargs to turn C‑style strings into Go strings and store them in the global argslice:
func goargs() {
if GOOS == "windows" {
return
}
argslice = make([]string, argc)
for i := int32(0); i < argc; i++ {
argslice[i] = gostringnocopy(argv_index(argv, i))
}
}The runtime.goenvs function (in runtime/os_linux.go) performs a similar conversion for environment variables, storing them in runtime.envs. Both argslice and envs are defined in runtime/runtime.go.
Reading Command‑Line Arguments
The saved arguments are not directly accessible because the variables are lowercase. The os package exposes them via the exported Args slice, initialized in os/proc.go:
package os
import "runtime"
// Args hold the command-line arguments, starting with the program name.
var Args []string
func init() {
if runtime.GOOS == "windows" {
// Initialized in exec_windows.go.
return
}
Args = runtime_args()
}
func runtime_args() []string // in package runtimeThe actual implementation links to os_runtime_args in runtime/runtime.go:
//go:linkname os_runtime_args os.runtime_args
func os_runtime_args() []string { return append([]string{}, argslice...) }Thus os.Args provides a copy of the original arguments, leaving the runtime’s internal slice untouched.
Command‑Line Argument Parsing with the flag Package
Go’s standard library offers the flag package for parsing arguments. Flags are defined with functions such as Bool, Int, String, etc., each returning a pointer to the value.
func Bool(name string, value bool, usage string) *bool
func Int(name string, value int, usage string) *int
func String(name string, value string, usage string) *string
// … and similar for other typesAlternatively, the Var functions bind a flag directly to an existing variable:
func BoolVar(p *bool, name string, value bool, usage string)
func IntVar(p *int, name string, value int, usage string)
// … and similar for other typesAfter defining flags, calling flag.Parse() populates them and makes the remaining non‑flag arguments available via flag.Args() and flag.NArg().
Parsing Process Details
The global flag.CommandLine (a FlagSet) holds the defined flags ( formal) and, after parsing, the actual values ( actual). The parsing algorithm proceeds flag by flag, updating the internal maps and stopping when any of the following conditions is met:
No more arguments.
The next argument is shorter than two characters.
The next argument does not start with ‘-’.
The next argument is exactly “--”.
Example output of the internal state before and after parsing demonstrates how CommandLine evolves.
Subcommands and Their Arguments
For programs with subcommands, a new FlagSet can be created for each subcommand. The article provides a sample where the main program parses -name and -age, then delegates remaining arguments to a subcommand hi with its own flag friend:
// sub_command_args.go
package main
import (
"flag"
"fmt"
)
var name = flag.String("name", "world", "user name")
var age int
func init() {
flag.IntVar(&age, "age", 0, "user age")
}
func main() {
flag.Parse()
fmt.Printf("hello %s, age is %d
", *name, age)
fmt.Printf("remaining args: %v
", flag.Args())
if flag.NArg() > 0 {
subCommand(flag.Args())
}
}
func subCommand(remainingArgs []string) {
switch remainingArgs[0] {
case "hi":
f := flag.NewFlagSet(remainingArgs[0], flag.ExitOnError)
friend := f.String("friend", "rose", "friend name")
if err := f.Parse(remainingArgs[1:]); err == nil {
fmt.Printf("%s's friend is %s
", *name, *friend)
} else {
f.Usage()
}
}
}Running the compiled program with appropriate arguments demonstrates how the subcommand’s flag is parsed independently.
Conclusion
The article has walked through the complete lifecycle of command‑line argument handling in Go—from low‑level kernel delivery, through runtime storage, to high‑level parsing with the flag package—providing code snippets, memory‑layout illustrations, and practical examples.
TiPaiPai Technical Team
At TiPaiPai, we focus on building engineering teams and culture, cultivating technical insights and practice, and fostering sharing, growth, and connection.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
