1 The Problem
We want a tool that takes some text — typed in or read from a file — and reports how many words, characters, and lines it has. It teaches the core string operations for breaking text into pieces and measuring them.
2 How to Think About It
Think about what “counting words” really means, before any code:
3 The Build — explained part by part
Here is the complete word counter. Each part is explained below.
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func countWords(text string) int { return len(strings.Fields(text)) }
func countChars(text string) int { return len(text) }
func main() {
fmt.Println("Paste your text, then press Enter twice to finish:")
var lines []string
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
if line == "" { // an empty line means "done"
break
}
lines = append(lines, line)
}
text := strings.Join(lines, "\n")
charCount := countChars(text)
wordCount := countWords(text) // Fields splits on any whitespace
lineCount := len(lines)
fmt.Printf("Characters: %d\n", charCount)
fmt.Printf("Words: %d\n", wordCount)
fmt.Printf("Lines: %d\n", lineCount)
}
scanner.Scan() returns true and loads the next line into scanner.Text(), or returns false when there is nothing left (or, here, when we break on a blank line).strings.Join(lines, "\n") — glue the collected lines back into one block of text, with a newline between each — the same job as Python’s
"\n".join(lines).len(text) — this is the one place Go and Python quietly differ: Go’s
len() on a string counts bytes, not visible characters. For plain English text every character is one byte, so it works here — but text with emoji or accented letters would need utf8.RuneCountInString instead (see the mistake below).strings.Fields(text) — this is the key move, and it matches Python’s
split() exactly: it splits on any run of whitespace (spaces, tabs, newlines) and ignores repeats, handing back a slice of words. len() of that slice is the word count.len(lines) — we already collected the lines into a slice, so counting them is just its length.
strings.Split(text, " ") with a literal space — then double spaces create empty “words”.strings.Fields(text); it handles any whitespace and ignores repeats.len(text) always means “number of characters” — it counts bytes, so text with é, €, or emoji will count higher than the number of characters a person sees.utf8.RuneCountInString(text) from the unicode/utf8 package instead.if line == "" { break } inside the loop, exactly as above.4 Test & Prove Each Part
We test the counting logic on text where we already know the answer — pulling countWords and countChars into their own functions makes this possible without typing anything by hand.
package main
import "testing"
func TestTwoWords(t *testing.T) {
if got := countWords("hello world"); got != 2 {
t.Errorf(`countWords("hello world") = %d; want 2`, got)
}
}
func TestEmpty(t *testing.T) {
if got := countWords(""); got != 0 {
t.Errorf(`countWords("") = %d; want 0`, got)
}
}
func TestExtraSpaces(t *testing.T) {
if got := countWords(" hello world "); got != 2 {
t.Errorf(`countWords(" hello world ") = %d; want 2`, got)
}
}
func TestCharCount(t *testing.T) {
if got := countChars("abc"); got != 3 {
t.Errorf(`countChars("abc") = %d; want 3`, got)
}
}
Run with go test -v ./.... The extra-spaces test is important: strings.Fields cleverly ignores repeated spaces, so “hello world” is still 2 words, not more — the same guarantee Python’s split() makes.
5 The Interface
What it expects
The quick brown fox
jumps over the lazy dog
[blank line]What it returns
Characters: 43
Words: 9
Lines: 26 Run It & Automate It
Save the code as wordcount.go and run it with go run wordcount.go — Go compiles and executes in one step, no separate build needed while you are experimenting.
go run wordcount.goType or paste text, then press Enter on a blank line to see the counts.
A CI tool like Jenkins runs go test automatically whenever the code changes — every line below has a plain explanation.
Paste your text, then press Enter twice to finish:
The quick brown fox
jumps over the lazy dog
Characters: 43
Words: 9
Lines: 2" ". Switch to strings.Fields(text).break as soon as scanner.Text() returns an empty string.// Jenkinsfile — runs the tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up Go') {
steps {
sh 'go version' // confirm Go is installed
sh 'test -f go.mod || go mod init word_counter' // create a module if none exists
}
}
stage('Run the tests') {
steps {
sh 'go vet ./...' // catch obvious mistakes before running
sh 'go test -v ./...' // run every test, show each result
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Read from a file. Use
os.ReadFileto count a whole document instead of typed text. (Teaches: file reading.) - Reading time. Estimate minutes to read (words ÷ 200). (Teaches: a calculation.)
- Most common word. Find which word appears most, using a
map[string]int. (Teaches: counting with a map.)
len() to measure bytes, strings.Fields() to break text into words, and strings.Join() to glue it back — plus the byte-vs-rune distinction that trips up a lot of newcomers to Go. Related: Strings & Runes, Standard Library.