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:
allowedDuration := totalDuration * time.Second
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:
func getNameContext(ctx context.Context) (string, error) { var err error name := "Default Name" c := make(chan error, 1) go func() { name, err = getName(os.Stdin, os.Stdout) c <- err }() select { case <-ctx.Done(): return name, ctx.Err() case err := <-c: return name, err } }
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.
The complete code is shown in Listing 2.7.
Listing 2.7: Implementing time-out for user input
// 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.
Next let's look at introducing another point of control in the program—the user. User signals are a way for the user to interrupt the normal workflow of a program. Two common user signals on Linux and MacOS are SIGINT
when the Ctrl+C key combination is pressed and SIGTERM
when the kill
command is executed. We want to allow the user to be able to cancel this external program at any point of time if the time-out hasn't already expired using either the SIGINT
or SIGTERM
signal.
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