Skip to main content
Create a text wrapper that intelligently breaks long text into lines of a maximum width, respecting word boundaries.

What You’ll Build

A text formatting program that:
  • Accepts long text and wraps it to a maximum width
  • Breaks lines at word boundaries (spaces)
  • Handles newlines in the original text
  • Works with Unicode characters (including Turkish)
  • Uses a fallthrough switch statement for control flow

What You’ll Learn

  • Iterating over strings with range (runes, not bytes)
  • Working with Unicode characters
  • Using the unicode package for character classification
  • Switch statements with fallthrough
  • Tracking state across loop iterations
  • Text formatting and line width management

The Problem

When you have long text, it might not fit well in a terminal or fixed-width display:
Galaksinin Batı Sarmal Kolu'nun bir ucunda, haritası bile çıkarılmamış ücra bir köşede, gözlerden uzak, küçük ve sarı bir güneş vardır. Bu güneşin yörüngesinde, kabaca yüz kırksekiz milyon kilometre uzağında, tamamıyla önemsiz ve mavi-yeşil renkli, küçük bir gezegen döner.
We want to wrap it to a specific width (e.g., 40 characters):
Galaksinin Batı Sarmal Kolu'nun bir
ucunda, haritası bile çıkarılmamış
ücra bir köşede, gözlerden uzak,
küçük ve sarı bir güneş vardır.

Bu güneşin yörüngesinde, kabaca yüz
kırksekiz milyon kilometre uzağında,
tamamıyla önemsiz ve mavi-yeşil
renkli, küçük bir gezegen döner.

Complete Solution

package main

import (
	"fmt"
	"unicode"
)

func main() {
	const text = `Galaksinin Batı Sarmal Kolu'nun bir ucunda, haritası bile çıkarılmamış ücra bir köşede, gözlerden uzak, küçük ve sarı bir güneş vardır.

Bu güneşin yörüngesinde, kabaca yüz kırksekiz milyon kilometre uzağında, tamamıyla önemsiz ve mavi-yeşil renkli, küçük bir gezegen döner.

Gezegenin maymun soyundan gelen canlıları öyle ilkeldir ki dijital kol saatinin hâlâ çok etkileyici bir buluş olduğunu düşünürler.`

	const maxWidth = 40

	var lw int // line width tracker

	for _, r := range text {
		fmt.Printf("%c", r)

		// Increment line width, then check conditions
		switch lw++; {
		case lw > maxWidth && r != '\n' && unicode.IsSpace(r):
			// Line too long and we're at a space (word boundary)
			fmt.Println()  // Start new line
			fallthrough    // Also reset line width
		case r == '\n':
			// Original text had a newline
			lw = 0  // Reset line width counter
		}
	}
	fmt.Println()
}

How It Works

1

Initialize Variables

const maxWidth = 40
var lw int  // line width counter
We track the current line width as we process characters.
2

Iterate Over Runes

for _, r := range text {
    fmt.Printf("%c", r)
    // ...
}
Using range on a string gives us runes (Unicode code points), not bytes. This correctly handles multi-byte characters like Turkish “ı”, “ş”, etc.
3

Track Line Width

switch lw++; {
case /* conditions */:
    // ...
}
The lw++ in the switch statement increments the counter before evaluating cases.
4

Break at Word Boundaries

case lw > maxWidth && r != '\n' && unicode.IsSpace(r):
    fmt.Println()  // Insert line break
    fallthrough    // Continue to next case
case r == '\n':
    lw = 0         // Reset counter
Logic:
  1. If line is too long AND we’re at a space (not newline)
    • Print a newline
    • Fall through to reset counter
  2. If we encounter a newline (original or inserted)
    • Reset the line width counter

Key Concepts

text := "Hello 世界"  // Mix of ASCII and Unicode

// ❌ Iterating over bytes (wrong for Unicode)
for i := 0; i < len(text); i++ {
    fmt.Printf("%c ", text[i])  // Breaks on 世界
}

// ✅ Iterating over runes (correct)
for _, r := range text {
    fmt.Printf("%c ", r)  // Works correctly
}
Why? Multi-byte characters like “世” use 3+ bytes. Iterating by byte breaks them.
switch x := getValue(); {
case x > 10:
    fmt.Println("Large")
    fallthrough  // Continue to next case
case x > 5:
    fmt.Println("Medium")
default:
    fmt.Println("Small")
}

// If x = 15:
// Prints "Large" then "Medium"
fallthrough forces execution of the next case, regardless of its condition.
import "unicode"

// Character classification
unicode.IsSpace(' ')   // true
unicode.IsSpace('\t')  // true
unicode.IsSpace('\n')  // true
unicode.IsSpace('a')   // false

unicode.IsLetter('a')  // true
unicode.IsLetter('ş')  // true (Turkish)
unicode.IsLetter('') // true (Chinese)

unicode.IsDigit('5')   // true
unicode.IsDigit('a')   // false
// You can initialize variables in switch
switch lw++; {  // Increment lw, then evaluate cases
case lw > maxWidth:
    // lw has already been incremented
}

// Equivalent to:
lw++
switch {
case lw > maxWidth:
    // ...
}

Algorithm Walkthrough

Let’s trace through an example:
text := "Hello world"
maxWidth := 8
lw := 0
Characterlw (before)lw (after)Action
H01Print “H”
e12Print “e”
l23Print “l”
l34Print “l”
o45Print “o”
(space)56Print ” “
w67Print “w”
o78Print “o”
r89lw > 8, next space → break line
l91Print “l” (on new line)
d12Print “d”
Output:
Hello wo
rld

Why This Approach?

Advantages:
  • Single pass through the text
  • No string building (efficient)
  • Handles Unicode correctly
  • Respects word boundaries
  • Preserves original newlines
  • Simple state management
Alternative approaches:
  • Split into words, then rebuild lines (less efficient)
  • Use regular expressions (overkill for this)
  • Buffer entire output (uses more memory)

Handling Edge Cases

// Word longer than maxWidth
"supercalifragilisticexpialidocious"  // Will exceed width

// Multiple consecutive spaces
"hello    world"  // Breaks at first space

// Only whitespace
"     "  // Each space might trigger a break

// Empty string
""  // Nothing to process
This simple algorithm doesn’t handle words longer than maxWidth. They will overflow the line. For a production wrapper, you’d need to handle this case (e.g., force-break long words).

Enhancements to Try

  1. Hyphenation - Break long words with hyphens
  2. Justification - Add spacing to make lines equal width
  3. Indentation - Indent first line or all lines
  4. Command-line args - Accept width and text as arguments
  5. Multiple paragraphs - Preserve double newlines
  6. Trim trailing spaces - Remove spaces at line ends
  7. Count lines - Report how many lines were created
  8. RTL support - Handle right-to-left languages

Configurable Width

Make the width configurable:
import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: wrapper <width>")
        return
    }
    
    maxWidth, err := strconv.Atoi(os.Args[1])
    if err != nil || maxWidth <= 0 {
        fmt.Println("Invalid width")
        return
    }
    
    // Rest of the code...
}

Testing Examples

# Test with different widths
go run main.go 20
go run main.go 60
go run main.go 80

# Test with special characters
go run main.go < turkish_text.txt
go run main.go < emoji_text.txt

Real-World Applications

  • Email clients - Wrapping plain text emails
  • Terminal apps - Formatting output for fixed-width displays
  • Chat apps - Wrapping long messages
  • Log viewers - Making logs more readable
  • README generators - Formatting documentation
  • Subtitle tools - Breaking subtitles into lines

Learning Resources

Unicode in Go

Official Go blog on strings, bytes, and runes

Unicode Package Docs

Full unicode package documentation

Next Steps

Spam Masker

More text processing practice

Strings Guide

Deep dive into Go strings

Build docs developers (and LLMs) love