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.