Читать книгу Practical Go - Amit Saha - Страница 31
Your First Application
ОглавлениеAll command-line applications essentially perform the following steps:
Accept user input
Perform some validation
Use the input to perform some custom task
Present the result to the user; that is, a success or a failure
In a command-line application, an input can be specified by the user in several ways. Two common ways are as arguments when executing the program and interactively by typing it in. First you will implement a greeter command-line application that will ask the user to specify their name and the number of times they want to be greeted. The name will be input by the user when asked, and the number of times will be specified as an argument when executing the application. The program will then display a custom message the specified number of times. Once you have written the complete application, a sample execution will appear as follows:
$ ./application 6 Your name please? Press the Enter key when done. Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool Nice to meet you Joe Cool
First, let's look at the function asking a user to input their name:
func getName(r io.Reader, w io.Writer) (string, error) { msg := "Your name please? Press the Enter key when done.\n" fmt.Fprintf(w, msg) scanner := bufio.NewScanner(r) scanner.Scan() if err := scanner.Err(); err != nil { return "", err } name := scanner.Text() if len(name) == 0 { return "", errors.New("You didn't enter your name") } return name, nil }
The getName()
function accepts two arguments. The first argument, r
, is a variable whose value satisfies the Reader
interface defined in the io
package. An example of such a variable is Stdin
, as defined in the os
package. It represents the standard input for the program—usually the terminal session in which you are executing the program.
The second argument, w
, is a variable whose value satisfies the Writer
interface, as defined in the io
package. An example of such a variable is the Stdout
variable, as defined in the os
package. It represents the standard output for the application—usually the terminal session in which you are executing the program.
You may be wondering why we do not refer to the Stdin
and Stdout
variables from the os
package directly. The reason is that doing so will make our function very unfriendly when we want to write unit tests for it. We will not be able to specify a customized input to the application, nor will we be able to verify the application's output. Hence, we inject the writer and the reader into the function so that we have control over what the reader, r
, and writer, w
, values refer to.
The function starts by using the Fprintf()
function from the fmt
package to write a prompt to the specified writer, w
. Then, a variable of Scanner
type, as defined in the bufio
package, is created by calling the NewScanner()
function with the reader, r
. This lets you scan the reader for any input data using the Scan()
function. The default behavior of the Scan()
function is to return once it has read the newline character. Subsequently, the Text()
function returns the read data as a string. To ensure that the user didn't enter an empty string as input, the len()
function is used and an error is returned if the user indeed entered an empty string as input.
The getName()
function returns two values: one of type string
and the other of type error
. If the user's input name was read successfully, the name is returned along with a nil
error. However, if there was an error, an empty string and the error is returned.
The next key function is parseArgs()
. It takes as input a slice of strings and returns two values: one of type config
and a second of type error
:
type config struct { numTimes int printUsage bool } func parseArgs(args []string) (config, error) { var numTimes int var err error c := config{} if len(args) != 1 { return c, errors.New("Invalid number of arguments") } if args[0] == "-h" || args[0] == "--help" { c.printUsage = true return c, nil } numTimes, err = strconv.Atoi(args[0]) if err != nil { return c, err } c.numTimes = numTimes return c, nil }
The parseArgs()
function creates an object, c
, of config
type to store this data. The config
structure is used for in-memory representation of data on which the application will rely for the runtime behavior. It has two fields: an integer field, numTimes
, containing the number of the times the greeting is to be printed, and a bool field, printUsage
, indicating whether the user has specified for the help message to be printed instead.
Command-line arguments supplied to a program are available via the Args
slice defined in the os
package. The first element of the slice is the name of the program itself, and the slice os.Args[1:]
contains the arguments that your program may care about. This is the slice of strings with which parseArgs()
is called. The function first checks to see if the number of command-line arguments is not equal to 1, and if so, it returns an empty config object and an error using the following snippet:
if len(args) != 1 { return c, errors.New("Invalid number of arguments") }
If only one argument is specified, and it is -h
or -help
, the printUsage
field is specified to true
and the object, c
, and a nil
error are returned using the following snippet:
if args[0] == "-h" || args[0] == "-help" { c.printUsage = true return c, nil }
Finally, the argument specified is assumed to be the number of times to print the greeting, and the Atoi()
function from the strconv
package is used to convert the argument—a string—to its integer equivalent:
numTimes, err = strconv.Atoi(args[0]) if err != nil { return c, err }
If the Atoi()
function returns a non-nil error value, it is returned; else numTimes
is set to the converted integer:
c.numTimes = numTimes
So far, we have seen how you can read the input from the user and read command-line arguments. The next step is to ensure that the input is logically valid; in other words, whether or not it makes sense for the application. For example, if the user specified 0
for the number of times to print the greeting, it is a logically incorrect value. The validateArgs()
function performs this validation:
func validateArgs(c config) error { if !(c.numTimes> 0) { return errors.New("Must specify a number greater than 0") } return nil }
If the value of the numTimes
field is not greater than 0
, an error is returned by the validateArgs()
function.
After processing and validating the command- line arguments, the application invokes the runCmd()
function to perform the relevant action based on the value in the config
object, c
:
func runCmd(r io.Reader, w io.Writer, c config) error { if c.printUsage { printUsage(w) return nil } name, err := getName(r, w) if err != nil { return err } greetUser(c, name, w) return nil }
If the field printUsage
is set to true
( -help
or -h
specified by the user), the printUsage()
function is called and a nil
error is returned. Otherwise, the getName()
function is called to ask the user to input their name.
If getName()
returned a non-nil error, it is returned. Else, the greetUser()
function is called. The greetUser()
function displays a greeting to the user based on the configuration supplied:
func greetUser(c config, name string, w io.Writer { msg := fmt.Sprintf("Nice to meet you %s\n", name) for i := 0; i < c.numTimes; i++ { fmt.Fprintf(w, msg) } }
The complete greeter application is shown in Listing 1.1.