Читать книгу Practical Go - Amit Saha - Страница 62
User Input with Deadlines
ОглавлениеLet's consider an example where your program asks for user input and the user has to type in the input and press the Enter key within 5 seconds else it will move on with a default name. Though a contrived example, it illustrates how you can enforce a time-out on any custom code in your application.
Let's look at the main()
function first:
func 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.