How to stop the fmt.Scanf process running under Goroutine?

Hello, I am working on a program that times out and moves to the next process if the user didn’t enter anything in the console after some time. I followed this post while designing my program, however, this doesn’t really cancel the process of fmt.Scanf and becomes problematic if there is another scanf after the timeout.

I created a modified version of the code which asks for a new input after the timeout:

func main() {
	ch := make(chan int)

	go func() {
		var age int
		fmt.Printf("Your age: ")
		fmt.Scanf("%d", &age)
		fmt.Println("Age: ", age)
		ch <- 1
	}()

	select {
	case <-ch:
		fmt.Println("Exiting.")
		os.Exit(0)
	case <-time.After(3 * time.Second):
		fmt.Println("\nTimed out")

		var height int
		fmt.Printf("Your height: ")
		fmt.Scanf("%d\n", &height)
		fmt.Println("Height: ", height)
	}
}

Here are some example inputs:

As you can see, when it asks for the height, it gets scanned from the age scanf, not the height scanf. This is because the age scanf is still active.

Now my question is, how do I stop the process of the first scanf? Or is there any other way to read user input that doesn’t wait and block until the user entered something in the console?

Ok, I did more digging after asking the question and found out there is no way to cancel the scanf (or read) process after it’s been called. This article explains it in great detail.

So I came up with a different approach. Instead of calling scanf right away, use the syscall.Select function to listen to the console input:

func Select(nfd int, r *FdSet, w *FdSet, e *FdSet, timeout *Timeval) (n int, err error)

As far as I know, what this essentially does is watch the given file descriptor(s) to see if they are ready for reading or writing within the timeout. It returns 1 if it’s readable or writable, otherwise 0. Now, this is a UNIX feature and may now work on some machines, especially on Windows. You can find more about Select here.

Using the select call, the program looks like this:

func main() {
	fmt.Printf("Your age: ")

	fd := 0 // stdin is 0 
	fds := syscall.FdSet{}
	FD_SET(&fds, fd)
	tv := syscall.Timeval{Sec: 3, Usec: 0} // 3 seconds timeout
	n, err := syscall.Select(fd+1, &fds, nil, nil, &tv)
	if err != nil {
		fmt.Println("Error: ", err)
		return
	}
	if n == 0 {
		fmt.Println("\nTimed out")

		var height int
		fmt.Printf("Your height: ")
		fmt.Scanf("%d", &height)
		fmt.Println("Height: ", height)
	} else if n == 1 {
		var age int
		fmt.Scanf("%d", &age)
		fmt.Println("Age: ", age)
	} else {
		fmt.Println("Error!")
	}
}

// by https://go.dev/play/p/LOd7q3aawd
func FD_SET(p *syscall.FdSet, i int) {
	p.Bits[i/64] |= 1 << (uint(i) % 64)
}

Works as intended!