β‘ Exercise 5: gofmt Modification - Indentation & AST Transformation
In this exercise, you’ll modify Go’s formatting tool gofmt
to use 4 spaces instead of tabs, and then add a custom AST transformation to automatically replace the word “hello” with “helo” in string literals and comments! π This will teach you how Go’s formatter works, how printer modes control indentation, and how to add custom transformations to the AST processing pipeline.
π― Learning Objectives
By the end of this exercise, you will:
- β Understand how gofmt controls indentation and printer modes
- β Learn to modify formatting behavior across gofmt and go/format package
- β Understand how gofmt processes Go source code through AST manipulation
- β Know how to modify string literals and comments in the AST
- β Explore Go’s AST (Abstract Syntax Tree) structure
- β Create custom source code transformations
π§ Background: How gofmt Works
gofmt operates through these stages:
- Parse β Convert source code to AST (Abstract Syntax Tree)
- Transform β Apply formatting rules to AST
- Print β Convert modified AST back to formatted source code with specific indentation
The indentation behavior is controlled by two key constants:
tabWidth
β Width of indentation (default: 8)printerMode
β Flags controlling spacing behavior:printer.UseSpaces
β Use spaces for paddingprinter.TabIndent
β Use tabs for indentationprinterNormalizeNumbers
β Normalize number literals
π³ AST Structure
Go represents source code as a tree of nodes we are going to use here this two nodes:
*ast.BasicLit
β String literals, numbers, etc.*ast.Comment
β Comments in source code
π Step 1: Navigate to gofmt Source
cd go/src/cmd/gofmt
ls -la
Key files:
gofmt.go
β Main program logic and file processingsimplify.go
β AST simplification transformations
π Step 2: Change Indentation to 4 Spaces
Before adding custom transformations, let’s change gofmt to use 4 spaces instead of tabs for indentation.
Modify gofmt.go
Edit go/src/cmd/gofmt/gofmt.go
:
Find the constants around line 47 (look for the comment “Keep these in sync with go/format/format.go”):
const (
tabWidth = 8
printerMode = printer.UseSpaces | printer.TabIndent | printerNormalizeNumbers
Change to:
const (
tabWidth = 4
printerMode = printer.UseSpaces | printerNormalizeNumbers
What changed:
tabWidth
: Changed from8
to4
(4 spaces per indentation level)printerMode
: Removedprinter.TabIndent
flag (this removes tab characters and uses spaces only)
Modify go/format Package
The go/format
package also needs to be updated to keep behavior consistent.
Edit go/src/go/format/format.go
:
Find the constants around line 28 (same comment as above):
const (
tabWidth = 8
printerMode = printer.UseSpaces | printer.TabIndent | printerNormalizeNumbers
Change to:
const (
tabWidth = 4
printerMode = printer.UseSpaces | printerNormalizeNumbers
π§ Understanding the Changes
tabWidth = 4
: Each indentation level uses 4 spaces- Removing
TabIndent
: Without this flag, the printer uses only spaces (no tab characters) UseSpaces
: Ensures spaces are used for padding and alignment- Both files must match: gofmt and go/format must use the same settings for consistency
π¨ Step 3: Rebuild and Test Indentation
cd ../../../ # back to go/src
./make.bash
Create a test file indent_test.go
:
package main
import "fmt"
func main() {
if true {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
}
Test the new indentation:
cd .. # to go/ directory
./bin/gofmt indent_test.go
Expected output (notice 4 spaces for each level):
package main
import "fmt"
func main() {
if true {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
}
π Each indentation level now uses 4 spaces instead of tabs!
Step 4: Add HelloβHelo Transformation
Edit gofmt.go
:
Add this transformation function around line 75 (after the usage()
function):
// transformHelloToHelo walks the AST and replaces "hello" with "helo"
// in string literals and comments.
func transformHelloToHelo(file *ast.File) {
ast.Inspect(file, func(n ast.Node) bool {
switch node := n.(type) {
case *ast.BasicLit:
// Handle string literals
if node.Kind == token.STRING {
if strings.Contains(node.Value, "hello") {
node.Value = strings.ReplaceAll(node.Value, "hello", "helo")
}
}
case *ast.Comment:
// Handle comments
if strings.Contains(node.Text, "hello") {
node.Text = strings.ReplaceAll(node.Text, "hello", "helo")
}
}
return true // continue traversing
})
}
π§ Understanding the Code
ast.Inspect()
- Traverses all nodes in the AST*ast.BasicLit
- Matches string literalsnode.Kind == token.STRING
- Checks if it’s a string (not a number)*ast.Comment
- Matches commentsstrings.ReplaceAll()
- Performs the replacement
Step 5: Integrate the Transformation
Still in gofmt.go
:
Find the processFile
function around line 256. Look for:
if *simplifyAST {
simplify(file)
}
Add our transformation right after:
if *simplifyAST {
simplify(file)
}
// Apply our custom helloβhelo transformation
transformHelloToHelo(file)
Step 6: Rebuild gofmt
cd ../../../ # back to go/src
./make.bash
Step 7: Test Both Modifications Together
Create a hello_test.go
file:
package main
import "fmt"
func main() {
// Say hello to everyone
message := "hello world"
greeting := "Say hello!"
/* This is a hello comment block */
fmt.Println(message)
fmt.Println(greeting)
// Another hello comment
fmt.Printf("hello %s\n", "Go")
}
../go/bin/gofmt hello_test.go
Expected output (notice both 4-space indentation AND helloβhelo transformation):
package main
import "fmt"
func main() {
// Say helo to everyone
message := "helo world"
greeting := "Say helo!"
/* This is a helo comment block */
fmt.Println(message)
fmt.Println(greeting)
// Another helo comment
fmt.Printf("helo %s\n", "Go")
}
π Two changes applied:
- All “hello” instances are replaced with “helo”
- Indentation uses 4 spaces instead of tabs
Step 8: Test In-Place Formatting
# Format and overwrite the file
../go/bin/gofmt -w hello_test.go
# Verify the changes
cat hello_test.go
The file is now permanently transformed with “helo” instead of “hello” and using 4-space indentation!
Understanding What We Did
- Modified Printer Settings: Changed tabWidth and printerMode to use 4 spaces
- Synced Two Packages: Updated both gofmt and go/format for consistency
- Added AST Visitor: Created function to traverse and modify AST nodes
- Pattern Matching: Identified string literals and comments
- Text Replacement: Modified node values to replace “hello” with “helo”
- Integration: Called transformation during gofmt processing
- Testing: Verified both indentation and transformation changes
π What We Learned
- π Printer Configuration: How gofmt controls indentation through tabWidth and printerMode
- π Package Consistency: Why gofmt and go/format must stay in sync
- π³ AST Manipulation: How to traverse and modify Go’s Abstract Syntax Tree
- π§ Tool Modification: How to extend existing Go tools with multiple changes
- π Code Transformation: Implementing systematic source code changes
- ποΈ Build Process: Rebuilding Go toolchain components
- π§ͺ Testing: Verifying custom tool behavior
π‘ Extension Ideas
Try these additional modifications: π
- β Add a command-line flag to enable/disable the transformation
- β Support multiple word replacements (helloβhelo, worldβuniverse)
- β Add case-sensitive option
- β Only replace whole words (not substrings within words)
- β Make tabWidth configurable via command-line flag
- β Add option to switch between tabs and spaces
Example flag addition:
var replaceHello = flag.Bool("helo", false, "replace hello with helo")
// In processFile():
if *replaceHello {
transformHelloToHelo(file)
}
Cleanup
To restore the original gofmt:
cd go/src/cmd/gofmt
git checkout gofmt.go
cd ../go/format
git checkout format.go
cd ../../../src
./make.bash
Summary
You’ve successfully modified gofmt in two powerful ways!
Indentation: tabs (8 width) β 4 spaces
Transformation: "hello world" β "helo world"
// Say hello β // Say helo
Changes: tabWidth=4 + remove TabIndent flag
+ ast.Inspect() β pattern match β replace text
You now understand how tools like gofmt
, goimports
, and go fix
work at both the printer and AST levels! β‘π³
Continue to Exercise 6 or return to the main workshop