Create a Boolean Yes or No prompt in Go
5 minutes
Or help me out by engaging with any advertisers that you find interesting
In this post, I will create a simple function that will allow you to ask the user a question, receive a valid response, and return it as a Boolean type. And if needed, the user will be able to tap Ctrl+C at any time to exit the program. Also importantly, despite using the operating system input, we will create unit tests for os.stdin.
go mod init devtidbits.com/yn
Firstly create a new function in the main.go file called YesNo that takes a string as an argument and returns a boolean value.
package main
func main() {}
func YesNo(s string) bool {
return false
}
Now let’s call the YesNo function by asking a question and printing the returned boolean value. At the moment, this will always be false.
func main() {
b := YesNo("Is the weather good today?")
fmt.Println(b)
}
func YesNo(s string) bool {
fmt.Printf("%s ", s)
return false
}
Create a new Parse function to read the rune and return the appropriate boolean or an error.
var ErrParse = errors.New("invalid keypress, it does not match a boolean")
// Parse the rune as a bool.
// Characters y or Y return true, n or N return false.
// Everything else returns an error.
func Parse(r rune) (bool, error) {
switch unicode.ToLower(r) {
case rune('y'):
return true, nil
case rune('n'):
return false, nil
}
return false, ErrParse
}
Update the YesNo function to read and parse the standard input. It also now prints the expected keyboard input.
// YesNo prints s requesting a boolean yes-or-no answer from the stardard input.
func YesNo(s string) bool {
fmt.Printf("%s [y/n] ", s)
input, err := Read(os.Stdin)
if err != nil {
log.Fatalln(err)
}
r, _ := Parse(input)
return r
}
$ go run .
Is the weather good today? [y/n] y
true
$ go run .
Is the weather good today? [y/n] n
false
Instead of manually inputting various keys to test the results, let’s automate it with unit tests. Create a main_test.go
file with a package main_test.
1package main_test
2
3import (
4 "io"
5 "testing"
6
7 main "devtidbits.com/yn"
8)
9
10func TestRead(t *testing.T) {
11 var stdin bytes.Buffer
12 tests := []struct {
13 name string
14 keypress string
15 want rune
16 wantErr bool
17 }{
18 // TODO: Add test cases.
19 }
20 for _, tt := range tests {
21 t.Run(tt.name, func(t *testing.T) {
22 stdin.WriteString(tt.keypress)
23 got, err := main.Read(&stdin)
24 if (err != nil) != tt.wantErr {
25 t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr)
26 return
27 }
28 if got != tt.want {
29 t.Errorf("Read() = %v, want %v", got, tt.want)
30 }
31 })
32 }
33}
The devtidbits.com/yn
module in the go.mod file is imported as main. The TestRead function is a boilerplate created by VSCode, and I’ve then modified it to simulate a standard input keypress.
Replace // TODO: Add test cases.
with our test arguments.
}{
{"empty", "", 0, true},
{"newline", "\n", rune('\n'), false},
{"space", " ", rune(' '), false},
{"upper y", "Y", rune('Y'), false},
{"lower y", "y", rune('y'), false},
{"multibyte", "😃", rune('😃'), false},
{"Yes", "Yes", rune('Y'), false},
}
The “empty” test returns an error because stdin cannot be null and always returns a value.
The “newline” and “space” tests confirm whitespace inputs work as expected. A newline control is the return value for the Enter or Return keypresses.
The “upper y” and “lower y” tests confirm case matching.
The “multibyte” test confirms characters larger than a single byte work correctly.
And the “Yes” test shows that only the first character of the standard input is ever read and returned.
Create a TestParse for the Parse function, mainly using the same TestRead cases. This test will be different though as incorrect inputs return errors.
func TestParse(t *testing.T) {
tests := []struct {
name string
r rune
want bool
wantErr bool
}{
// incorrect values
{"empty", 0, false, true},
{"newline", rune('\n'), false, true},
{"space", rune(' '), false, true},
{"multibyte", rune('😃'), false, true},
// correct values
{"Yes", rune('Y'), true, false},
{"yes", rune('y'), true, false},
{"No", rune('N'), false, false},
{"no", rune('n'), false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := main.Parse(tt.r)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
}
}
Don’t forget to run the tests.
$ go test .
ok devtidbits.com/yn 0.002s
Bonus, limit the keypresses.
Currently, YesNo
returns false
for all keypresses except for
Y or
Caps Lock+Y. Assuming false
for all other keys presses is generally bad for usability and accidental taps. Instead, YesNo
should only work with the
N and
Y keys. Otherwise, it should remind the user of the question.
1// YesNo prints s requesting a boolean yes-or-no answer from the stardard input.
2func YesNo(s string) bool {
3 for {
4 fmt.Printf("%s [y/n] ", s)
5 input, err := Read(os.Stdin)
6 if err != nil {
7 log.Fatalln(err)
8 }
9 if r, err := Parse(input); err == nil {
10 return r
11 }
12 }
13}
$ go run .
Is the weather good today? [y/n]
Is the weather good today? [y/n] x
Is the weather good today? [y/n] ?
Is the weather good today? [y/n] y
true
Bonus, intercept Ctrl+C with a graceful exit
A user can tap Ctrl+C to quit during the Parse loop, which causes an ugly signal interrupt exit.
$ go run .
Is the weather good today? [y/n] ^Csignal: interrupt
Create a new CtrlC function that will intercept the signal and gracefully exit with an emoji handwave. We must also call this function in YesNo before its input loop.
// CtrlC intercepts any Ctrl+C keyboard input and exits to the shell.
func CtrlC() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Fprintf(os.Stdout, " 👋\n")
os.Exit(0)
}()
}
1func YesNo(s string) bool {
2 CtrlC()
3 for {
Is the weather good today? [y/n] ^C 👋
That’s it, nice work, and thank you for reading. The complete source code is on my GitHub repo.

Written by Ben Garrett