Taking User Input in Go

Taking User Input in Go

#GO-getter series

At the start of learning any language, we don't have any means to get the data such as an API. It is also not advisable to build API just to learn the nuances of language. So, taking the user inputs from the console is the best way to mimic the user interaction with the application. Without the user input, you will mostly operate with your application in isolation. That's why each language has different ways to get the user input from the terminal. So does GO.

There are many ways to get user input in Go, but here we will discuss one that unfolds a lot of things that accompany it. Get your hands on the keyboard and type the code written below:

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    welcome := "Welcome to the automatic feedback system"
    fmt.Println(welcome)

    fmt.Println("Please share a few words of review. It helps us to improve our service.")
    reader := bufio.NewReader(os.Stdin)

    input, _ := reader.ReadString('\n')

    fmt.Printf("Thanks for your review: %v", input)
}

If you run the code above, it prints the statements provided to the Go print function which is Println() and directs the cursor to the new line that indicates that it expects input from the user. After writing something to the console and hitting enter will take the input and execute the program.

Regardless of the language, in programming, the assignment always happens from right to left. So we try to understand in the same flow. We first encounter the os.Stdin on the rightmost side. Stdin has to do with the standard input which is part of standard streams in computer programming. If we get to the Wikipedia page of the standard streams, it describes it somewhat like this:

In short, Stdin stream performs read operation from the text terminal. This raises one more query that Println() function from fmt package prints data to the console then it must be using a standard output stream under the hood. Let's find out. Go to https://pkg.go.dev/ and search fmt in search box. It will lead to the whole new page of results. You must be seeing fmt as the top result. Click on that and you will be on the documentation page of the fmt package. There will be an index as shown below.

A click on func Println() will redirect you to the link of func Println() on the same page. Click on that link to get into the implementation of that function which looks something like this:

Voila, we can see that Println() internally calls Fprintln() function that takes os.Stdout a standard output stream as an argument. Hence, Println() internally uses an output stream to write data to the terminal.

Now, if we get back to the code, it has a parenthesized import statement, an obvious syntax of Go, that groups all the packages from which the import of the function happens. We will pick up where we left off, that is os.Stdin . We are using something called Stdin from package os. Head over to https://pkg.go.dev/ and search os takes us to the standard library documentation result. Select Varaibles from the index and you will see:

var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

From above we can say that Stdin is a package level variable as it is capitalised. NewFile() is the function from the same os package that says:

Here, file descriptor fd is syscall.Stdin that is converted to uintptr . If you find syscall package from Go docs https://pkg.go.dev/ then Stdin is variable with 0 file descriptor. This is simply because the posix standard is that stdin is attached to the first file descriptor, 0. The Wikipedia page for file descriptor says:

To cut a long story short, as per the docs, the package os provides a platform-independent interface to operating system functionality. The design is Unix-like, although the error handling is Go-like; failing calls return values of type error rather than error numbers. Since stdin is always available and open by default, os.NewFile can just turn this file descriptor into an os.File, and uses the standard Linux filepath "/dev/stdin" as an easily recognizable file name.

So, whatever you write into the console will directly be written to /dev/stdin file mimicking the Linux standard streams and provided to bufio.NewReader() . bufio, yet another package provided by Go implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O.

What on earth is buffering and buffered I/O? Wikipedia page for buffer describes buffer as:

Buffered I/O is the technique in which functions maintain their internal buffer (cache) of a certain size while performing I/O operations. It is the technique of temporarily storing the results of an I/O operation in user-space(buffer) before transmitting it to the kernel (in the case of writes) or before providing it to your process (in the case of reads). By doing so, we can reduce the system calls thus improving the application performance.

Imagine a process that writes one character at a time to a file. This approach is notably inefficient. Every write operation triggers a write() system call, necessitating a journey into the kernel, a memory copy, albeit just for a single byte, and a return to the user-space, all to subsequently repeat the entire process. Worse, filesystems and storage devices work in terms of blocks. Thus, buffering could read a large chunk of the file into a buffer, and then dole out small pieces of it as requested by the process.

Take a look at the implementation of bufio.NewReader() .

Function NewReader() returns pointer to Reader via NewReaderSize() which internally maintains buffer buf that is the slice of byte. b, ok := rd.(*Reader) is the comma ok syntax performs the type assertion on rd to assert that it is the the type of Reader struct.

If it is so, the returned reader will be stored in b and ok evaluates to true. First if condition checks that ok is true and buf member of reader b is greater or equal to the defaultBufSize then it returns a struct b . If the first condition returns false then NewReader() simply returns Reader of minReadBufferSize.

So, bufio.NewReader(os.Stdin) returns buffer Reader that contains the bytes of information which can be read through ReadString() function of bufio package that accepts delim (in our case, a new line) and reads until the first occurrence of delim in the input, returning a string containing the data up to and including the delimiter. If ReadString encounters an error before finding a delimiter, it returns the data read before the error and the error itself (often io.EOF).

input, _ := reader.ReadString('\n') is the comma ok syntax as discussed earlier where "blank indentifier" _ is used. The real use of Blank Identifier comes when a function returns multiple values, but we need only a few values and want to discard some values. Basically, if we use variable name instead of blank identifier then Go compiler throws an error as we are not using that variable anywhere after the declaration and initialization. So blank identifier hides that value and makes it unusable.

In this way, we have gone through the things beneath the surface while receiving the user input in Go. Happy Learning! See you soon.