Practical Go. Amit Saha
Чтение книги онлайн.
Читать онлайн книгу Practical Go - Amit Saha страница 11
testing
package exclusively for writing all of the tests, and we will use Go test to drive all of the test executions. We have also used the excellent support provided by libraries such as net/http/httptest
to test HTTP clients and servers. Similar support is provided by gRPC libraries. In the last chapter, we will use a third-party package, https://github.com/testcontainers/testcontainers-go
, to create local testing environments using Docker Desktop.
In some of the tests, especially when writing command-line applications, we have adopted the style of “Table Driven Tests,” as described at https://github.com/golang/go/wiki/TableDrivenTests
, when writing the tests.
Summary
In this introduction to the book, you installed the software necessary to build the various applications to be used in the rest of the book. Then I introduced some of the conventions and assumptions made throughout the remainder of the book. Finally, I described the key language features with which you will need to be familiar to make the best use of the material in the book.
Great! You are now ready to start your journey with Chapter 1, where you will be learning how to build testable command-line applications.
CHAPTER 1 Writing Command-Line Applications
In this chapter, you will learn about the building blocks of writing command-line applications. You will use standard library packages to construct command-line interfaces, accept user input, and learn techniques to test your applications. Let's get started!
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: