Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1. A Tutorial Introduction

This chapter is a tour. It moves fast, shows real programs, and leaves the careful definitions for later chapters. The goal is to get enough Go into your hands that the rest of the book has something to build on. Anything you do not fully understand here is explained properly further along, and the text says where.

1.1 Getting Started

The only way to start.

package main

import "fmt"

func main() {
	fmt.Println("hello, world")
}

Put that in a file called hello.go and run go run hello.go. It compiles the program to a temporary file, runs it, and throws the binary away. go build hello.go keeps the binary instead, producing an executable called hello that depends on nothing and can be copied to another machine with the same operating system and run there.

In practice you work in a module rather than on loose files:

go mod init hello

That writes a go.mod naming the module and the Go version it expects. Once one exists, go run . builds the package in the current directory, go build ./... builds everything beneath it, and go get can add dependencies. Every program in this book lives in the module under examples.

The program is three ideas. package main says this file belongs to the package named main, the one that produces an executable; any other name produces a library. import "fmt" pulls in the formatted-I/O package from the standard library. func main() is the function the system calls to start the program, and a main package must have one.

Before you run anything else, run gofmt -w hello.go. It rewrites the file in Go’s standard layout: tabs for indentation, braces where Go puts braces, imports sorted. Nobody configures it, because it takes no options. Every Go project you will ever read has been through it, so they all look alike, and arguments about formatting are simply not a thing that happens. Most editors run it on save; set that up now and never think about it again.

Exercise 1-1. Run the program. Then delete the import line and compile again. Read the error.

Exercise 1-2. Print several lines. Find out what \n, \t, and \" do inside the quotes, and what backquotes do instead of quotes.

1.2 Variables and Arithmetic Expressions

The next program prints a table of Fahrenheit temperatures and their Celsius equivalents, using the formula C = (5/9)(F−32).

// Print a Fahrenheit-Celsius table for fahr = 0, 20, ..., 300.
package main

import "fmt"

func main() {
	const lower, upper, step = 0, 300, 20

	fahr := lower
	for fahr <= upper {
		celsius := 5 * (fahr - 32) / 9
		fmt.Printf("%d\t%d\n", fahr, celsius)
		fahr += step
	}
}

It prints:

0	-17
20	-6
40	4
...
300	148

Comments run from // to the end of the line, and /* ... */ spans lines. A comment directly above a top-level declaration is its documentation, which go doc will show to anyone who asks about it, so write those two facing the reader rather than yourself.

const introduces values fixed at compile time; lower, upper, and step never change, so they are constants rather than variables. The line declares three of them at once.

fahr := 0 is the short declaration. It declares a variable and infers its type from the value, and it is how Go programmers declare nearly everything. The long form is var fahr int = 0, useful when you want to name the type, and var fahr int alone gives you Go’s zero value for that type: 0 for numbers, "" for strings, and false for booleans. Every variable in Go starts out as something; there is no uninitialized memory and no undefined.

Two things about := will catch you. It only works inside a function, so package-level declarations use var or const. And it must declare at least one new name, so a second := on the same variable is an error rather than an assignment:

shortdecl/main.go:7:7: no new variables on left side of :=

Now the surprise that catches everyone. Declare a variable and fail to use it, and the program does not compile:

unusedvar/main.go:7:2: declared and not used: total

An unused import is the same:

unusedimport/main.go:5:2: "os" imported and not used

These are errors, not warnings, and there is no flag to soften them. It feels petty for about a week and then it stops registering, because the thing it prevents is real: unused names are the residue of a change you did not finish, and in a language that lets them slide they accumulate until nobody can tell which are dead. When you genuinely want a value you are not using yet, assign it to _, the blank identifier, which discards it.

The arithmetic is where this program lies to you. 5 * (fahr - 32) / 9 is integer arithmetic throughout, so the division truncates toward zero and the table is wrong in the last digit. C has the same trap and K&R spends the same paragraph on it. The fix is floating point.

1.3 Formatted Output

// The same table in floating point, with the columns lined up.
package main

import "fmt"

func main() {
	const lower, upper, step = 0.0, 300.0, 20.0

	fahr := lower
	for fahr <= upper {
		celsius := (fahr - 32.0) * 5.0 / 9.0
		fmt.Printf("%3.0f %6.1f\n", fahr, celsius)
		fahr += step
	}
}
  0  -17.8
 20   -6.7
 40    4.4
...
300  148.9

Printf formats its arguments according to a template, in the C tradition, and its verbs are worth learning early because you will use them all day. %d is an integer, %f a float, %s a string, %t a boolean. %v is any value in its default format and %+v adds field names when the value is a struct, which between them cover most debugging. %q is a quoted string, which shows you the whitespace you are staring past. %T prints the value’s type rather than the value, and is the fastest way to answer “what did that expression actually give me”. A literal percent sign is %%.

Between the % and the verb goes the width and precision: %6.1f means at least six characters wide with one digit after the point, and a minus sign as in %-6s left-justifies instead. That is what lines the columns up above.

Println needs no template. It prints its arguments separated by spaces with a newline at the end, and it is what you reach for when the layout does not matter. fmt.Sprintf returns the formatted string instead of printing it.

Notice that the constants changed from 0 to 0.0. Go will not mix numeric types in an expression, and it will not quietly convert one to the other:

mismatch/main.go:8:14: invalid operation: n * f (mismatched types int and float64)

Every conversion is written out, as float64(n). This is tedious in the small and worth it in the large, because it means the type of every expression is readable from the expression itself.

Exercise 1-3. Add a heading to the table.

Exercise 1-4. Print a Celsius-to-Fahrenheit table.

1.4 The For Statement

There are many ways to write any program. Here is the table again, counting down, in the form a Go programmer would actually use.

// The table again, as a Go programmer would write it.
package main

import "fmt"

func main() {
	for fahr := 300; fahr >= 0; fahr -= 20 {
		fmt.Printf("%3d %6.1f\n", fahr, (float64(fahr)-32)*5/9)
	}
}

Go has one loop keyword, and for covers every shape other languages spread across three. The full form has an initializer, a condition, and a post statement, with no parentheses around them and non-optional braces:

for i := 0; i < 10; i++ {

Drop the first and third and you have a while:

for fahr <= upper {

Drop all three and you have a loop that runs until something inside it breaks:

for {

Ranging over something gives you the elements. Over a slice or array you get index and value, over a map key and value, over a string index and rune, over a channel each value sent, and over an integer the numbers below it:

for i := range 10 {

That last one is how power counts in section 1.8. When you want only the second half of a two-value range, _ discards the first: for _, v := range xs.

i++ is a statement rather than an expression, so you cannot write x = i++, and there is no ++i. Assignment operators like += work as you would expect.

1.5 Character Input and Output

This program copies its input to its output, a line at a time.

// Copy standard input to standard output, a line at a time.
package main

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

func main() {
	in := bufio.NewScanner(os.Stdin)
	for in.Scan() {
		fmt.Println(in.Text())
	}
}

os.Stdin is the program’s input as an open file. bufio.NewScanner wraps it in a scanner that buffers reads and splits the stream into tokens, lines by default. Scan advances to the next token and reports whether it found one, so the loop ends at end of input; Text returns the token it found, without the newline. Splitting on words instead is one call: in.Split(bufio.ScanWords).

Two habits to pick up now. Reading a whole file into memory with os.ReadFile is fine for a configuration file and a bad idea for a log, while a scanner works the same on both. And Println writes to standard output while fmt.Fprintln(os.Stderr, ...) writes to standard error, where diagnostics belong so that a program’s real output stays pipeable.

1.6 Counting

The Unix wc program counts lines, words, and characters. Here it is.

// Count lines, words, and characters on standard input.
package main

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

func main() {
	lines, words, chars := 0, 0, 0

	in := bufio.NewScanner(os.Stdin)
	for in.Scan() {
		line := in.Text()
		lines++
		words += len(strings.Fields(line))
		chars += len(line) + 1
	}

	fmt.Printf("%d %d %d\n", lines, words, chars)
}
$ go run ./ch01/count < gettysburg.txt
4 54 309

Which agrees with the real wc. Three variables are declared and assigned on one line, which reads well when they belong together and badly when they do not. strings.Fields splits on runs of whitespace and returns a slice of the pieces, so len of it is the word count. The character count adds one per line for the newline the scanner removed.

chars counts bytes, not characters, and the two are the same only while the input is ASCII. Go strings are UTF-8, and Chapter 7 takes that seriously.

Exercise 1-5. Print the longest line in the input.

Exercise 1-6. Count how many times each word appears. You will want a map, covered in section 7.3; try it after reading that.

1.7 Functions

// Test the power function.
package main

import "fmt"

// power raises base to the n-th power, for n >= 0.
func power(base, n int) int {
	p := 1
	for range n {
		p *= base
	}
	return p
}

func main() {
	for i := range 10 {
		fmt.Printf("%d %6d %6d\n", i, power(2, i), power(-3, i))
	}
}
0      1      1
1      2     -3
2      4      9
...
9    512 -19683

A function declaration is func, the name, the parameters with their types, and the result type. Where consecutive parameters share a type you write it once, so (base, n int) is two integers. The result type goes after the parameter list, where Lux writes -> int and C writes the type in front.

Go functions can return more than one value, and a function that might fail reports it that way, a convention running through the whole standard library. Chapters 4 and 8 are about it.

Names starting with a capital letter are exported from their package and visible to code that imports it; names starting lower-case are private to the package. Println is capitalized for that reason and power is not. Case is the only access-control mechanism Go has, so you can see a name’s visibility at every use rather than only at its declaration.

1.8 Command-Line Arguments

// Echo the command-line arguments.
package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	fmt.Println(strings.Join(os.Args[1:], " "))
}

os.Args is a slice of strings holding the command line, with the program’s own name at index 0, so the arguments proper are os.Args[1:]. Slicing is section 7.1. For anything beyond a couple of positional arguments the standard library’s flag package parses options for you, and Chapter 10 uses it.

Exercise 1-7. Write a program that prints its arguments one per line, with the index of each.

Exercise 1-8. Write echo: print the arguments separated by spaces, but accept a -n first argument meaning “no trailing newline”.

1.9 What You Have

You can now write a Go program that reads input, does arithmetic, loops, branches, calls functions you wrote, and prints results. That is most of what any program does. The rest of this book is about the parts Go asks you to be deliberate about: what a type is and what conversions cost, what happens when you pass a value to a function, how a program says that something went wrong, and how to write code that works on more than one type. Then the tooling, and then concurrency.