02 Command Source Files

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.

  1. By default, what types of parameter values can we allow the command source code file to accept?
  2. 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.