Amit Saha

Practical Go


Скачать книгу

main() { allowedDuration := totalDuration * time.Second ctx, cancel := context.WithTimeout(context.Background(), allowedDuration) defer cancel() name, err := getNameContext(ctx) if err != nil && !errors.Is(err, context.DeadlineExceeded) { fmt.Fprintf(os.Stdout, "%v\n", err) os.Exit(1) } fmt.Fprintln(os.Stdout, name) }

      The function creates a new context using the context.WithTimeout() function. The context.WithTimeout() function accepts two arguments: the first is a parent Context object and the second is a time.Duration object specifying the time—in milliseconds, seconds, or minutes—after which the context will expire. Here we set the time-out to be 5 seconds:

      Next, we create the Context object:

      ctx, cancel := context.WithTimeout(context.Background(), allowedDuration) defer cancel()

      Since we don't have another context that will play the role of the parent context, we create a new empty context using context.Background(). The WithTimeout() function returns two values: the created context, ctx, and a cancellation function, cancel. It is necessary to call the cancellation function in a deferred statement so that it is always called just before the function returns. Then we call the getNameContext() function as follows:

      name, err := getNameContext(ctx)

      If the error returned was the expected context.DeadlineExceeded, we do not show it to the user and just display the name; else we show it and exit with a non-zero exit code:

      if err != nil && !errors.Is(err, context.DeadlineExceeded) { fmt.Fprintf(os.Stdout, "%v\n", err) os.Exit(1) } fmt.Fprintln(os.Stdout, name)

      Now let's look at the getNameContext() function:

      The overall idea of the implementation of this function is as follows:

      1 Execute the getName() function in a goroutine.

      2 Once the function returns, write the error value into a channel.

      3 Create a select..case block to wait on a read operation on two channels:The channel that is written to by the ctx.Done() functionThe channel that is written to when the getName() function returns

      4 Depending on which of step a or b above completes first, either the context deadline exceeded error is returned along with the default name or the values returned by the getName() function are returned.

      // chap2/user-input-timeout/main.go package main import ( "bufio" "context" "errors" "fmt" "io" "os" "time" ) var totalDuration time.Duration = 5 func getName(r io.Reader, w io.Writer) (string, error) { scanner := bufio.NewScanner(r) msg := "Your name please? Press the Enter key when done" fmt.Fprintln(w, msg) scanner.Scan() if err := scanner.Err(); err != nil { return "", err } name := scanner.Text() if len(name) == 0 { return "", errors.New("You entered an empty name") } return name, nil } // TODO Insert getNameContext() definition as above // TODO Insert main() definition as above

      Create a new directory, chap2/user-input-timeout, and initialize a module inside it:

      $ mkdir -p chap2/user-input-timeout $ cd chap2/user-input-timeout $ go mod init github.com/username/user-input-timeout

      Next, save Listing 2.7 as main.go. Build it as follows:

      $ go build -o application

      Run the program. If you do not input any name within 5 seconds, you will see the following:

      $ ./application Your name please? Press the Enter key when done Default Name

      However, if you input a name and press Enter within 5 seconds, you will see the name that was entered:

      $ ./application Your name please? Press the Enter key when done

       John C

      John C

      You learned to use the WithTimeout() function to create a context that allows you to enforce a limit which is relative to the current time. The WithDeadline() function, on the other hand, is useful when you want to enforce a real-world deadline. For example, if you wanted to ensure that a function must be executed before June 28, 10:00 AM, you could use a context created via WithDeadline() .

      Next, you will learn to test such timeout behavior in your applications as part of Exercise 2.3.

       EXERCISE 2.3: UNIT TESTING THE TIME-OUT EXCEEDED BEHAVIOR Write a test to verify the time-out exceeded behavior. One straightforward way to do so is not to provide any input at all in the test so that the deadline exceeds. Of course, you should also test the “happy path,” that is, where you provide an input and the deadline doesn't exceed. It is recommended to use a shorter time-out—in the order of a few 100 milliseconds to avoid time-consuming tests.

      Handling User Signals

      We touched upon the fact that a number of standard library functions accept a context as a parameter. Let's see how it works using the os/exec package's execCommandContext() function. One situation in which this becomes useful is when you want to enforce a maximum time of execution for these commands. Once again, this can be implemented by using a context created via the WithTimeout() function:

      package main import ( "context" "fmt" "os" "os/exec" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := exec.CommandContext(ctx, "sleep", "20").Run(); err != nil { fmt.Fprintln(os.Stdout, err) }

      }

      When run on Linux/MacOS, the above code snippet will yield the following error:

      signal: killed

      The CommandContext() function force kills an external program when the context expires. In the above code, we set up a context that will be canceled after 10 seconds. We then used the context to execute a command "sleep", "20", which will sleep for 20 seconds. Hence the command is killed. Thus, in a scenario where you want your application to execute external commands but want to have a guaranteed behavior that the commands must finish execution in a certain amount of time, you can achieve it using the technique above.

      Here are the steps involved in doing this:

      1 Create a context using the WithTimeout() function.

      2 Set up a signal handler that will create a handler for the SIGINT and SIGTERM signal. When one of the signals is received, the