Chapter 6 - Slices

Exercise 1: The Slice Operator

Define a printLines function that takes a slice of strings as a parameter, and prints each element of that slice on a separate line.

Then, in main, get a slice of the daysOfWeek array containing just the weekdays: “Monday”, “Tuesday”, “Wednesday”, “Thursday”, and “Friday”. Pass that slice to printLines.

package main

import "fmt"

// YOUR CODE HERE: Define a printLines function.

func main() {
	daysOfWeek := [7]string{"Sunday", "Monday", "Tuesday",
		"Wednesday", "Thursday", "Friday", "Saturday"}
	// YOUR CODE HERE: Get a slice containing just the
	// weekdays.
	// Pass that slice to printLines.
}

Output:

Monday
Tuesday
Wednesday
Thursday
Friday

When you’re ready, have a look at our solution.

Exercise 2: Appending to a Slice

The program below should ask the user to input several strings. When the user is done, it should output the strings the user entered, separated by commas. Comments within the code explain how it all works. The finished program will rely on the strings.Join function to join the strings together. You can read documentation on strings.Join here.

Your task is to update the code in the main function to get it all working. Set up a variable with a slice of strings to hold the phrases the user enters. Add new phrases to the end of the slice. And call strings.Join to join the strings in the finished slice with commas.

Sample output:

String to add: cashews
Continue? [Y/n]: y
String to add: peanuts
Continue? [Y/n]: y
String to add: almonds
Continue? [Y/n]: n
cashews, peanuts, almonds
package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"strings"
)

// Here we use a package variable, something we haven't
// done much of. It's in scope within all functions in the
// main package. This allows us to set up a reader just once,
// at the start of the "main" function, and then keep
// re-using it for the life of the program.
var reader *bufio.Reader

// getString prompts the user to input a string. Returns
// an empty string and an error if there was a problem
// getting input, or the user's input and a nil error
// otherwise.
func getString(question string) (string, error) {
	// Display the question and prompt for the answer
	// on the same line.
	fmt.Print(question + ": ")
	input, err := reader.ReadString('\n')
	// If there was an error, just return it to the caller.
	if err != nil {
		return "", err
	}
	// Remove the newline from the end.
	input = strings.TrimSpace(input)
	// Return the input and a "nil" error.
	return input, nil
}

// askToContinue prompts the user if they want to continue.
// If they enter "y", "Y", or no input, it will return true.
// If they enter "n" or "N", it will return false.
func askToContinue() bool {
	// This uses a "for" loop without any condition, which
	// just loops forever (until the user enters an accepted
	// answer and we return).
	for {
		answer, err := getString("Continue? [Y/n]")
		// If there was an error, log it and exit.
		if err != nil {
			log.Fatal(err)
		}
		// If user entered "Y" or "N", convert it to lower-case.
		answer = strings.ToLower(answer)
		// If user entered "n", don't continue.
		if answer == "n" {
			return false
			// If user entered "y" or nothing, continue.
		} else if answer == "y" || answer == "" {
			return true
		}
	}
}

func main() {
	// Store the Reader that getString will use in the
	// package variable. Note this is an assignment, not a
	// declaration!
	reader = bufio.NewReader(os.Stdin)
	// Process at least one phrase
	more := true
	// YOUR CODE HERE: Set up a slice to hold phrases the
	// user enters.
	
	// Loop until user chooses not to continue.
	for more == true {
		// Get the user's input.
		input, err := getString("String to add")
		// If there was an error, log it and exit.
		if err != nil {
			log.Fatal(err)
		}
		// YOUR CODE HERE: Add the user's input to the
		// slice.
		
		// Ask if the user wants to continue.
		more = askToContinue()
	}
	// YOUR CODE HERE: Call strings.Join with your slice
	// of strings, and the string ", " to join everything
	// together.
	
}

Here’s our solution.

Exercise 3: Command-line Arguments

CSV is a common file format used to represent spreadsheet data in plain-text files. Go includes an encoding/csv package that can read this format.

We’re working on a program that accepts the name of a CSV file and a column number as command-line arguments. The program should go through the file and print only the requested column.

So, for example, if you save the following data as gophers.csv:

first_name,last_name,username
"Rob","Pike",rob
Ken,Thompson,ken
"Robert","Griesemer","gri"

And you run the program with pcolumn gophers.csv 2, it should output:

last_name
Pike
Thompson
Griesemer

We’ve written the printColumn function for you, which reads the specified file, prints the specified column, and returns a non-nil error value if it encounters any problems. We’ve also written a check function that will log an error an exit the program, unless the error is nil.

Your task is to update the main function to read the two command-line arguments, check that they’re valid, and use them to make an appropriate call to printColumn. We’ve added comments to main that describe what you’ll need to do. You’ll need to call strconv.ParseInt to parse the column number argument, so you might need to refer to the strconv.ParseInt documentation briefly.

// pcolumn prints the contents of a specified column from a
// CSV file. It takes two command-line arguments: the name
// of the file to read, and the column number to print.
// Example:
//
// pcolumn my.csv 2
package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"log"
	"os"
	"strconv"
)

// printColumn reads the specified file and prints the
// specified column from each row. A non-nil return
// value indicates an error was encountered.
func printColumn(fileName string, column int) error {
	// Open the file for reading, and return any error.
	file, err := os.Open(fileName)
	if err != nil {
		return err
	}
	// Ensure the file gets closed, even if there's an
	// error. We'll talk about "defer" in chapter 12.
	defer file.Close()
	// Set up a new csv.Reader that reads from the file.
	reader := csv.NewReader(file)
	// Loop until an error is encountered.
	for {
		// Read the next row.
		row, err := reader.Read()
		// Reaching the end of the file is an "error", but
		// it's an expected one, so just return.
		if err == io.EOF {
			return nil
		} else if err != nil {
			// Return any other type of error, because
			// those are NOT expected.
			return err
		}
		// If a column outside the row was requested,
		// return an error.
		if int(column) > len(row) {
			return fmt.Errorf("invalid column: %d", column)
		}
		// Otherwise, print the requested column.
		fmt.Println(row[column-1])
	}
}

// check logs an error and exits if the error is not nil.
func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	// YOUR CODE HERE: Check the length of os.Args to see
	// if the user gave exactly two command-line arguments.
	// If not, log an error message and exit. Remember that
	// the first element of os.Args is always the name of
	// the program that was run.
	// The first argument will be the file name.
	// Call strconv.ParseInt with the second command-line
	// argument, a base of 10, and a bitSize of 64.
	// Pass the error value from parseInt to "check", so
	// any error will be reported.
	// Call printColumn with the file name and column
	// number. Note that ParseInt returns an int64 and
	// printColumn wants an int, so you'll need to cast
	// the type.
	// Call "check" on the error value returned from
	// printColumn.
}

Here’s our solution.