A Go program reads six test scores, adds them up, and prints a mean of 0.0. The line above it says count: 6, so every number arrived. The compiler accepted the file without a word, and go vet, the standard tool for spotting suspicious code, printed nothing at all. Save the scores in scores.txt, one per line: 72, 85, 91, 64, 78, 67. The correct mean is 76.2, which is 457 over 6.
// scores.go
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scores := []int{}
for scanner.Scan() {
n, err := strconv.Atoi(scanner.Text())
if err != nil {
continue
}
scores = append(scores, n)
}
total := 0
if len(scores) > 0 {
total := 0
for _, s := range scores {
total += s
}
}
fmt.Printf("count: %d\n", len(scores))
fmt.Printf("mean: %.1f\n", float64(total)/float64(len(scores)))
}
$ go run scores.go < scores.txt
count: 6
mean: 0.0
The cause is the second total := 0. It declares a new variable that lives only inside the if block, so the loop fills that one and the outer total stays 0.
Key takeaways
package main,import, andfunc mainare the three parts of a runnable Go file, and a file withoutfunc mainfails withfunction main is undeclared in the main package.var name typedeclares without a value, and Go fills the variable with that type's zero value:0,"",false,0, or a nil slice.:=declares and assigns in one statement and takes the type from the right-hand side.- A
:=inside a block declares a new variable that ends at the closing brace, even when the same name exists outside, which is what shadowing means. - Go refuses to build a program with a declared and unused variable (
declared and not used: bonus), but a shadowed variable counts as used, so the compiler stays quiet. go vetprinted nothing at all on the shadowed program, so the wrong mean survived every automatic check.
The three parts of a Go file
Every program here starts with package main, which names the package the file belongs to. main is the package go run starts from. Next comes import, which lists the packages the file uses. In scores.go, fmt prints text and strconv turns text into numbers.
Then comes func main, the function that runs first. A file without it fails to build with function main is undeclared in the main package.
Declaring variables
Save this as main.go and run it. The first five lines of main use var, which declares a name and a type without a value.
// main.go
package main
import "fmt"
func main() {
var total int
var name string
var ready bool
var ratio float64
var list []string
fmt.Printf("int=%d string=%q bool=%v float=%v slice=%v\n", total, name, ready, ratio, list == nil)
scores := []int{72, 85, 91}
sum := 0
for _, s := range scores {
sum += s
}
fmt.Printf("sum=%d mean=%.1f\n", sum, float64(sum)/float64(len(scores)))
limit := 5
if limit > 0 {
limit := 10
fmt.Println("inside the block:", limit)
}
fmt.Println("after the block:", limit)
}
$ go run main.go
int=0 string="" bool=false float=0 slice=true
sum=248 mean=82.7
inside the block: 10
after the block: 5
Go fills each var with the zero value of its type: 0 for an integer, "" for a string (the %q verb prints the quotes), false for a bool, and 0 for a float. A slice is a list that can grow. One declared with var is nil and has length 0, which is why list == nil prints slice=true.
The := form declares and assigns in one statement, and the type comes from the value on the right. So scores := []int{72, 85, 91} is a slice of three ints. Their sum is 248, and 248 divided by 3 is 82.666…, which %.1f prints as 82.7.
When := makes a second variable
A block is the code between a pair of braces. The last four lines of main.go declare limit := 5 and then declare limit := 10 inside an if block. Inside a block, := with a name that already exists outside creates a new variable that ends at the closing brace. The output shows both: 10 inside, 5 after. Go calls this shadowing.
That is the bug in scores.go. The inner total is the one the loop adds to, and the closing brace throws it away. The Printf at the bottom reads the outer total, which is still 0.
The one rule the compiler enforces
Go refuses to build a program with an unused variable. Save this as unused.go. It prints one line and declares a bonus that nothing reads:
// unused.go
package main
import "fmt"
func main() {
fmt.Println("three scores")
bonus := 5
}
$ go run unused.go
# command-line-arguments
./unused.go:8:2: declared and not used: bonus
The error names the file, the line, and the column, so ./unused.go:8:2 points at line 8, column 2 of unused.go. The program exits with status 1. The rule is about declarations, not values: the compiler checks that every declared name is mentioned somewhere. A name used inside a block counts as used. The inner total is used by total += s, so scores.go compiles.
Check the fix with a second program
You spotted the wrong mean because you already knew the answer. A checker makes that comparison mechanical. Save this as check_main.go. It reads another program's output from standard input and compares it with two expected lines.
// check_main.go
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
want := []string{"count: 6", "mean: 76.2"}
got := []string{}
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := strings.TrimRight(scanner.Text(), "\r")
if line != "" {
got = append(got, line)
}
}
passed, failed := 0, 0
for i := range want {
if i < len(got) && got[i] == want[i] {
fmt.Printf("PASS line %d: %s\n", i+1, want[i])
passed++
continue
}
if i < len(got) {
fmt.Printf("FAIL line %d: expected %q, got %q\n", i+1, want[i], got[i])
} else {
fmt.Printf("FAIL line %d: expected %q, no such line\n", i+1, want[i])
}
failed++
}
fmt.Printf("\n%d of %d checks passed\n", passed, len(want))
if failed > 0 {
os.Exit(1)
}
}
The pipe (|) sends the output of scores.go into the checker's input. Run it against the broken version:
$ go run scores.go < scores.txt | go run check_main.go
PASS line 1: count: 6
FAIL line 2: expected "mean: 76.2", got "mean: 0.0"
1 of 2 checks passed
exit status 1
The checker exited with status 1, and go run printed that as the last line. Now delete the second total := 0 from scores.go, so the loop adds to the total declared above the block:
// scores.go
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scores := []int{}
for scanner.Scan() {
n, err := strconv.Atoi(scanner.Text())
if err != nil {
continue
}
scores = append(scores, n)
}
total := 0
if len(scores) > 0 {
for _, s := range scores {
total += s
}
}
fmt.Printf("count: %d\n", len(scores))
fmt.Printf("mean: %.1f\n", float64(total)/float64(len(scores)))
}
$ go run scores.go < scores.txt | go run check_main.go
PASS line 1: count: 6
PASS line 2: mean: 76.2
2 of 2 checks passed
The checker now exits with status 0, so a script can gate on it.
I declare a variable in the innermost block that contains every line that reads or writes it. In a deeper block I change it with = or +=, because := there starts a new variable that ends at the next closing brace.