02 Command Source Files #
We already know that the environment variable GOPATH points to one or more workspaces, each of which contains source code files organized in the form of code packages.
These source code files can be divided into three types: command source code files, library source code files, and test source code files, each with different purposes and writing rules. (I introduced the basic information of these three file types in the “Prerequisites” section.)
(Long press to view the large image)
Today, we will dive deeper into the knowledge of command source code files.
When we start learning to write programs in a programming language, we always hope to receive feedback in a timely manner during the coding process so that we can clearly understand whether it is right or wrong. In fact, our effective learning and progress are achieved through continuous feedback and adjustments.
For Go language learners, during the learning stage, you will often write programs that can be executed directly. Such programs will certainly involve the writing of command source code files, and command source code files can also be conveniently started using the go run
command.
So, my question for today is: What is the purpose of a command source code file, and how do we write it?
Here, I will provide you with a reference answer: A command source code file is the entry point of a program and is a requirement for any program that can be run independently. We can generate an executable file corresponding to it by building or installing, and the latter is generally named after the immediate parent directory of the command source code file.
If a source code file declares itself as a member of the main
package and contains a main
function with no arguments and no return values, then it is a command source code file. Just like the following code:
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
If you save this code as demo1.go
, then you will see Hello, world!
printed on the screen (standard output) after running the go run demo1.go
command.
When modular programming is needed, we often split the code into multiple files, and even split them into different code packages. However, for an independent program, there can only be one and only one command source code file. If there are other source code files in the same package as the command source code file, they should also declare themselves as members of the
main
package.
Problem Analysis #
Command source code files are so important that they undoubtedly become our first assistant in learning Go language. However, it is not enough to just print Hello, world
. We should not become a “Hello, world” party. Since you have decided to learn Go language, you should delve into every knowledge point.
Whether it’s Linux or Windows, if you have used the command line, you must know that almost all commands can accept arguments. By building or installing command source code files, the generated executable files can be regarded as “commands”. Since they are commands, they should have the ability to accept arguments.
Now, let me take you on a deep dive into a series of issues related to receiving and parsing command arguments.
In-depth Explanation #
1. How to receive arguments in the command source code file #
Let’s first look at an incomplete code snippet:
package main
import (
// Add code here. [1]
"fmt"
)
var name string
func init() {
// Add code here. [2]
}
func main() {
// Add code here. [3]
fmt.Printf("Hello, %s!\n", name)
}
If I invite you to help me and add the corresponding code at the comments, and make the program implement the functionality of “greeting someone based on the arguments given when running the program,” what would you do?
If you know how to do it, please start implementing it now. If you don’t know, don’t worry, we’ll figure it out together.
First of all, there is a package in the Go standard library specifically used to receive and parse command arguments. This package is called flag
.
As I mentioned before, if you want to use program entities from a package in your code, you should import that package first. So, we need to add the code "flag"
at [1]
. Note that you should enclose the import path with English double quotes. With this change, the code now imports both the flag
and fmt
packages.
Next, the name of the person to greet must be represented by a string. So we need to add code that calls the StringVar
function from the flag
package at [2]
. Here’s an example:
flag.StringVar(&name, "name", "everyone", "The greeting object.")
The flag.StringVar
function takes four parameters.
The first parameter is the address where the value of the command argument should be stored. In this case, it is the address of the variable name
declared earlier, represented by the expression &name
.
The second parameter is used to specify the name of the command argument, which is name
in this case.
The third parameter is used to specify the default value when the command argument is not provided. In this case, it is everyone
.
As for the fourth function parameter, it is a short description of the command argument, which will be used when printing the command instructions.
By the way, there is another function similar to flag.StringVar
called flag.String
. The difference between these two functions is that the latter returns a pre-allocated address for storing the command argument value directly. If we use this function, we would need to change the following code:
var name string
to
var name = flag.String("name", "everyone", "The greeting object.")
So, if we use the flag.String
function, we would need to modify the original code. This does not meet the requirements of the problem mentioned earlier.
Now let’s talk about the last blank. We need to add the code flag.Parse()
at [3]
. The flag.Parse
function is used to actually parse the command arguments and assign their values to the corresponding variables.
The function call to flag.Parse
must be placed after the declaration of all command argument storage facilities (here, it’s the variable name
) and the setup (here, the call to flag.StringVar
at [2]
), and before reading any command argument values.
Because of this, it is best to place flag.Parse()
at the beginning of the function body of the main
function.
2. How to pass arguments when running the command source code file, and how to view the usage instructions #
If we save the above code as a file named demo2.go
, we can pass a value to the name
argument by running the following command:
go run demo2.go -name="Robert"
After running this command, the content printed to the standard output (stdout) will be:
Hello, Robert!
Additionally, if you want to view the usage instructions of this command source code file, you can do the following:
$ go run demo2.go --help
Here, the $
represents running the go run
command after the command prompt. The output of this command will be similar to:
Usage of /var/folders/ts/7lg_tl_x2gd_k1lm5g_48c7w0000gn/T/go-build155438482/b001/exe/demo2:
-name string
The greeting object. (default "everyone")
exit status 2
You may not understand the meaning of the output code below.
/var/folders/ts/7lg_tl_x2gd_k1lm5g_48c7w0000gn/T/go-build155438482/b001/exe/demo2
This is the full path of the executable file temporarily generated when building the above command source code file with the go run
command.
If we first build this command source code file and then run the generated executable file, like this:
$ go build demo2.go
$ ./demo2 --help
The output will be:
Usage of ./demo2:
-name string
The greeting object. (default "everyone")
3. How to customize the usage instructions of a command source code file #
There are many ways to do this, and the simplest way is to reassign the variable flag.Usage
. flag.Usage
is a func()
type, which means a function type with no parameter and no result declaration.
flag.Usage
variable is already assigned when declared, so we can see the correct result when running the command go run demo2.go --help
.
Note that the assignment to flag.Usage
must be done before calling the flag.Parse
function.
Now, let’s save demo2.go as demo3.go, and add the following code at the beginning of the main
function body:
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")
flag.PrintDefaults()
}
Then, when running
$ go run demo3.go --help
you will see:
Usage of question:
-name string
The greeting object. (default "everyone")
exit status 2
Now let’s go deeper. When we call some functions in the flag
package (such as StringVar
, Parse
, etc.), we are actually calling the corresponding methods of the flag.CommandLine
variable.
flag.CommandLine
is equivalent to the command parameter container by default. Therefore, by reassigning flag.CommandLine
, we can customize the usage instructions of the current command source code file at a deeper level.
Now, uncomment the assignment statement to flag.Usage
variable in the main
function body, and add the following code at the beginning of the init
function body:
flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
flag.CommandLine.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")
flag.PrintDefaults()
}
After running the command go run demo3.go --help
, the output will be the same as the previous output. However, this customized method is more flexible. For example, when we change the second argument value passed to the flag.NewFlagSet
function to flag.PanicOnError
:
flag.CommandLine = flag.NewFlagSet("", flag.PanicOnError)
and then run the go run demo3.go --help
command, it will produce a different output. This is because we are passing flag.PanicOnError
as the second argument value to the flag.NewFlagSet
function. flag.PanicOnError
and flag.ExitOnError
are constants predefined in the flag
package.
The meaning of flag.ExitOnError
is to tell the command parameter container that when --help
is followed after the command or when the parameter settings are incorrect, the current program will end with the status code 2
after printing the command parameter usage instructions.
The status code 2
represents a user error in using the command, while flag.PanicOnError
differs from it by raising a “runtime panic”.
Both of the above cases will be triggered when we call the flag.Parse
function. By the way, “runtime panic” is a concept in Go program error handling. I will discuss it in the later part of this column.
Now, let’s go further. Instead of using the global flag.CommandLine
variable, let’s create a private command parameter container. Add another variable declaration outside of the function:
var cmdLine = flag.NewFlagSet("question", flag.ExitOnError)
Then, replace the call to flag.StringVar
with cmdLine.StringVar
, and replace flag.Parse()
with cmdLine.Parse(os.Args[1:])
.
os.Args[1:]
refers to the command parameters we provided. This completely overrides flag.CommandLine
. The *flag.FlagSet
type variable cmdLine
has many interesting methods. You can explore them. I won’t explain them one by one here.
The advantage of doing this is still to customize the command parameter container more flexibly. But more importantly, your customization will not affect that global variable flag.CommandLine
.
Summary
Congratulations! You have now taken the first step in Go programming. You can write commands in Go and use them like many operating system commands, and even embed them into various scripts.
Although I have explained the basic method of writing command source code files to you, and also talked about the various preparations needed to accept parameters, this is not all.
Don’t worry, I will mention it frequently later. Also, if you want to learn more about the usage of the flag
package, you can check the documentation at this website. Alternatively, you can directly use the godoc
command to start a Go language documentation server locally. How to use the godoc
command? You can refer to here.
Thought Exercise #
We have already seen how to pass string type parameter values into command source code files. Can we pass anything else? That is the thought exercise I leave you with today.
- By default, what types of parameter values can we allow the command source code file to accept?
- Can we use custom data types as parameter value types? If yes, how?
You can find the answer to the first question by consulting the documentation. Remember, the ability to quickly look up and understand documentation is a necessary skill.
As for the second question, it may be difficult for you to answer because it involves another question: “How can we declare our own data types?” I will address this question in a later part of the column. If that is the case, I hope you take note of it along with the question mentioned here and answer it after you can solve the latter.
Click here to view the detailed code accompanying the Go language column.