1.4. Big Digits—Two-Dimensional Slices
The bigdigits program (in file bigdigits/bigdigits.go) reads a number entered on the command line (as a string), and outputs the same number onto the console using “big” digits. Back in the twentieth century, at sites where lots of users shared a high-speed line printer, it used to be common practice for each user’s print job to be preceded by a cover page that showed some identifying details such as their username and the name of the file being printed, using this kind of technique.
We will review the code in three parts: first the imports, then the static data, and then the processing. But right now, let’s look at a sample run to get a feel for how it works:
Each digit is represented by a slice of strings, with all the digits together represented by a slice of slices of strings. Before looking at the data, here is how we could declare and initialize single-dimensional slices of strings and numbers:
Slices have the form Type, and if we want to initialize them we can immediately follow with a brace-delimited comma-separated list of elements of the corresponding type. We could have used the same variable declaration syntax for both, but have used a longer form for the lowPrimes slice to show the syntactic difference and for a reason that will be explained in a moment. Since a slice’s Type can itself be a slice type we can easily create multidimensional collections (slices of slices, etc.).
The bigdigits program needs to import only four packages.
The fmt package provides functions for formatting text and for reading formatted text (§3.5, → 93). The log package provides logging functions. The os package provides platform-independent operating-system variables and functions including the os.Args variable of type string (slice of strings) that holds the command-line arguments. And the path package’s filepath package provides functions for manipulating filenames and paths that work across platforms. Note that for packages that are logically inside other packages, we only specify the last component of their name (in this case filepath) when accessing them in our code.
For the bigdigits program we need two-dimensional data (a slice of slices of strings). Here is how we have created it, with the strings for digit 0 laid out to illustrate how a digit’s strings correspond to rows in the output, and with the strings for digits 3 to 8 elided.
Variables declared outside of any function or method may not use the := operator, but we can get the same effect using the long declaration form (with keyword var) and the assignment operator (=) as we have done here for the bigDigits variable (and did earlier for the lowPrimes variable). We still don’t need to specify bigDigits’ type since Go can deduce that from the assignment.
We leave the bean counting to the Go compiler, so there is no need to specify the dimensions of the slice of slices. One of Go’s many conveniences is its excellent support for composite literals using braces, so we don’t have to declare a data variable in one place and populate it with data in another—unless we want to, of course.
The main() function that reads the command line and uses the data to produce the output is only 20 lines.
The program begins by checking to see if it was invoked with any command-line arguments. If it wasn’t, len(os.Args) will be 1 (recall that os.Args holds the program’s name, so the slice’s length is normally at least 1), and the first if statement (18 ←, ➊) will be satisfied. In this case we output a suitable usage message using the fmt.Printf() function that accepts % placeholders similar to those supported by the C/C++ printf() function or by Python’s % operator. (See §3.5, → 93 for full details.)
The path/filepath package provides path manipulation functions—for example, the filepath.Base() function returns the basename (i.e., the filename) of the given path. After outputting the message the program terminates using the os.Exit() function and returns 1 to the operating system. On Unix-like systems a return value of 0 is used to indicate success, with nonzero values indicating a usage error or a failure.
The use of the filepath.Base() function illustrates a nice feature of Go: When a package is imported, no matter whether it is top-level or logically inside another package (e.g., path/filepath), we always refer to it using only the last component of its name (e.g., filepath). It is also possible to give packages local names to avoid name collisions; Chapter 9 provides the details.
If at least one command-line argument was given, we copy the first one into the stringOfDigits variable (of type string). To convert the number that the user entered into big digits we must iterate over each row in the bigDigits slice to produce each line of output, that is, the first (top) string for each digit, then the second, and so on. We assume that all the bigDigits’ slices have the same number of rows and so take the row count from the first one. Go’s for loop has various syntaxes for different purposes; here (18←, ➋ and 18←, ➌) we have usedfor ... range loops that return the index positions of each item in the slices they are given.
The row and column loops part of the code could have been written like this:
This is a form familiar to C, C++, and Java programmers and is perfectly valid in Go.5 However, the for ... range syntax is shorter and more convenient. (Go’s for loops are covered in §5.3, → 201.)
At each row iteration we set that row’s line to be an empty string. Then we iterate over the columns (i.e., the characters) in the stringOfDigits string we received from the user. Go strings hold UTF-8 bytes, so potentially a character might be represented by two or more bytes. This isn’t an issue here because we are only concerned with the digits 0, 1, ..., 9 each of which is represented by a single byte in UTF-8 and with the same byte value as in 7-bit ASCII. (We will see how to iterate over a string character by character—regardless of whether the characters are single-or multibyte—in Chapter 3.)
When we index a particular position in a string we get the byte value at that position. (In Go the byte type is a synonym for the uint8 type.) So we retrieve the byte value of the command-line string at the given column and subtract the byte value of digit 0 from it to get the number it represents (18←, ➍). In UTF-8 (and 7-bit ASCII) the character '0' is code point (character) 48 decimal, the character '1' is code point 49, and so on. So if, for example, we have the character '3' (code point 51), we can get its integer value by doing the subtraction '3' -'0' (i.e., 51 − 48) which results in an integer (of type byte) of value 3.
Go uses single quotes for character literals, and a character literal is an integer that’s compatible with any of Go’s integer types. Go’s strong typing means we cannot add, say, an int32 to an int16 without explicit conversion, but Go’s numeric constants and literals adapt to their context, so in this context '0' is considered to be a byte.
If the digit (of type byte) is in range (18←, →) we can add the appropriate string to the line. (In the if statement the constants 0 and 9 are considered to be bytes because that’s digit’s type, but if digit was of a different type, say, int, they would be treated as that type instead.) Although Go strings are immutable (i.e., they cannot be changed), the += append operator is supported to provide a nice easy-to-use syntax. (It works by replacing the original string under the hood.) There is also support for the + concatenate operator which returns a new string that is the concatenation of its left and right string operands. (The string type is covered fully in Chapter 3.)
To retrieve the appropriate string (19←, ➏) we access the bigDigits’s slice that corresponds to the digit, and then within that to the row (string) we need.
If the digit is out of range (e.g., due to the stringOfDigits containing a nondigit), we call the log.Fatal() function with an error message. This function logs the date, time, and error message—to os.Stderr if no other log destination is explicitly specified—and calls os.Exit(1) to terminate the program. There is also a log.Fatalf() function that does the same thing and which accepts % placeholders. We didn’t use log.Fatal() in the first if statement (18←, ➊) because we want to print the program’s usage message without the date and time that the log.Fatal() function normally outputs.
Once all the number’s strings for the given row have been accumulated the complete line is printed. In this example, seven lines are printed because each digit in the bigDigits slice of strings is represented by seven strings.
One final point is that the order of declarations and definitions doesn’t generally matter. So in the bigdigits/bigdigits.go file we could declare the bigDigits variable before or after the main() function. In this case we have put main() first since for the book’s examples we usually prefer to order things top-down.
The first two examples have covered a fair amount of ground, but both of them show material that is familiar from other mainstream languages even though the syntax is slightly different. The following three examples take us beyond the comfort zone to illustrate Go-specific features such as custom Go types, Go file handling (including error handling) and functions as values, and concurrent programming using goroutines and communication channels.