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.