⚑ 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:

🧠 Background: How gofmt Works

gofmt operates through these stages:

  1. Parse β†’ Convert source code to AST (Abstract Syntax Tree)
  2. Transform β†’ Apply formatting rules to AST
  3. Print β†’ Convert modified AST back to formatted source code with specific indentation

The indentation behavior is controlled by two key constants:

🌳 AST Structure

Go represents source code as a tree of nodes we are going to use here this two nodes:

πŸ” Step 1: Navigate to gofmt Source

cd go/src/cmd/gofmt
ls -la

Key files:

πŸ“ 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:

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

πŸ”¨ 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

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:

  1. All “hello” instances are replaced with “helo”
  2. 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

  1. Modified Printer Settings: Changed tabWidth and printerMode to use 4 spaces
  2. Synced Two Packages: Updated both gofmt and go/format for consistency
  3. Added AST Visitor: Created function to traverse and modify AST nodes
  4. Pattern Matching: Identified string literals and comments
  5. Text Replacement: Modified node values to replace “hello” with “helo”
  6. Integration: Called transformation during gofmt processing
  7. Testing: Verified both indentation and transformation changes

πŸŽ“ What We Learned

πŸ’‘ Extension Ideas

Try these additional modifications: πŸš€

  1. βž• Add a command-line flag to enable/disable the transformation
  2. βž• Support multiple word replacements (helloβ†’helo, worldβ†’universe)
  3. βž• Add case-sensitive option
  4. βž• Only replace whole words (not substrings within words)
  5. βž• Make tabWidth configurable via command-line flag
  6. βž• 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