How to Pad a String in Go (Left and Right)

Pad strings in Go using fmt.Sprintf width specifiers or strings.Repeat for custom characters.

When columns won't line up

You are building a CLI tool that lists files with their sizes. The output looks messy because filenames have different lengths. You need every name to take up exactly 20 characters so the size column lines up perfectly. Or maybe you are generating a CSV where the first column must be exactly 10 digits, padded with zeros. String padding is the fix.

Go handles padding in two ways. The fmt package provides width specifiers for quick alignment with spaces or zeros. The strings package gives you manual control when you need custom padding characters or complex logic. Both approaches allocate new strings, since Go strings are immutable.

Width specifiers in fmt

The standard library includes padding support inside fmt.Sprintf. You specify a width in the format verb, and Go adds characters to reach that width. The default padding character is a space, and the default alignment is right-aligned, which means padding goes on the left.

Here is the simplest way to pad a string using format verbs.

package main

import "fmt"

func main() {
    // Target width is 10 characters.
    width := 10
    name := "Go"

    // %10s pads on the left with spaces to reach width 10.
    leftPadded := fmt.Sprintf("%10s", name)

    // %-10s pads on the right with spaces to reach width 10.
    rightPadded := fmt.Sprintf("%-10s", name)

    // %010s pads on the left with zeros instead of spaces.
    zeroPadded := fmt.Sprintf("%010s", name)

    fmt.Println(leftPadded, rightPadded, zeroPadded)
}

The number in the verb sets the minimum width. The s tells fmt to treat the argument as a string. If the string is shorter than the width, Go adds padding. If the string is already longer, fmt prints the full string without truncation. Width specifiers control alignment, not truncation.

Flags and dynamic width

The format verb supports flags that change padding behavior. The minus flag - flips alignment to the left, putting padding on the right. The zero flag 0 changes the padding character from space to 0. You can combine flags. %0-10s pads with zeros on the right.

Sometimes the width is not known until runtime. You can pass the width as an argument using the * flag. This replaces the width number in the verb with a value from the argument list.

package main

import "fmt"

func main() {
    // Width is determined at runtime.
    width := 12
    label := "ID"

    // The * consumes the width argument and applies it to the padding.
    dynamic := fmt.Sprintf("%*s", width, label)

    // You can also pass the width for a number, padding with zeros.
    code := fmt.Sprintf("%0*d", width, 42)

    fmt.Println(dynamic, code)
}

The * flag is useful when building format strings dynamically. It avoids string concatenation to build the verb itself. The width argument must be an integer. If you pass a string where an integer is expected, the compiler rejects the program with cannot use "width" (untyped string constant) as int value in argument.

Custom padding characters

fmt only supports space and zero for padding. If you need dots, hashes, or any other character, you must build the padding manually. The strings.Repeat function generates a repeated substring, which you can concatenate with your original string.

Here is how to pad with a custom character using strings.Repeat.

package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "data"
    width := 10
    padChar := "."

    // Calculate how many padding characters are needed.
    needed := width - len(s)

    // Repeat the padding character and prepend it for left padding.
    leftPadded := strings.Repeat(padChar, needed) + s

    // Append the repeated padding for right padding.
    rightPadded := s + strings.Repeat(padChar, needed)

    fmt.Println(leftPadded, rightPadded)
}

This approach gives you full control over the padding character. You can use any string as the padding unit, including multi-character patterns. The calculation width - len(s) determines the count. If the string is longer than the width, needed becomes negative, and strings.Repeat panics with strings.Repeat: negative count. You must check the length before repeating.

A safe helper function

In real code, you often need a reusable function that handles custom padding and protects against negative counts. A helper function encapsulates the logic and prevents panics.

Here is a realistic helper for generating fixed-width strings with custom padding.

package main

import (
    "fmt"
    "strings"
)

// padLeft adds custom characters to the left of a string.
// It returns the original string unchanged if it already meets the width.
func padLeft(s string, width int, padChar string) string {
    // Calculate the difference between target width and current length.
    diff := width - len(s)

    // If the string is already long enough, return it without modification.
    if diff <= 0 {
        return s
    }

    // Generate the padding and prepend it to the original string.
    return strings.Repeat(padChar, diff) + s
}

func main() {
    // Use padLeft to create a fixed-width ID with custom padding.
    id := padLeft("A12", 8, "X")
    fmt.Println(id)
}

The function checks diff <= 0 before calling strings.Repeat. This ensures the program never panics if the input string exceeds the target width. The receiver name convention does not apply here since this is a standalone function, but if you attached this to a type, you would use a short receiver name like (p *Padder) PadLeft(...).

The byte trap

Go strings are UTF-8 encoded byte sequences. The len function returns the number of bytes, not the number of characters. If your string contains multi-byte characters like emojis or accented letters, len returns a value larger than the visual character count. Padding calculations based on len will be off.

For example, the string café has four characters but five bytes because é takes two bytes. len("café") returns 5. If you pad to width 6, you get only one padding character, which looks misaligned.

The fmt package also counts width in bytes, not runes. %10s ensures the output is at least 10 bytes long. If you care about visual alignment with multi-byte text, you need to count runes and pad based on rune count.

package main

import (
    "fmt"
    "strings"
    "unicode/utf8"
)

func main() {
    s := "café"
    targetRunes := 8

    // Count runes instead of bytes for accurate visual width.
    runeCount := utf8.RuneCountInString(s)
    needed := targetRunes - runeCount

    // Pad with spaces based on rune count.
    // Each space is one byte and one rune, so this works for ASCII padding.
    padded := s + strings.Repeat(" ", needed)

    fmt.Println(padded)
}

Using utf8.RuneCountInString gives you the character count. You can then calculate padding based on runes. This matters when your data includes non-ASCII text. The convention in Go is to accept that len is fast and byte-oriented. Use utf8 helpers only when character semantics matter.

Performance considerations

fmt.Sprintf parses the format string every time it runs. This parsing has a small cost. If you are padding strings in a tight loop processing millions of records, the overhead can add up. strings.Repeat is faster because it skips format parsing.

For maximum performance, pre-compute padding strings if the width is constant. You can store a lookup table of padding strings and concatenate them. This avoids allocation in the hot path.

package main

import (
    "fmt"
    "strings"
)

func main() {
    // Pre-compute padding strings for common widths.
    padding := make([]string, 20)
    for i := range padding {
        padding[i] = strings.Repeat(" ", i)
    }

    // Use the pre-computed padding in a loop.
    s := "Go"
    width := 10
    needed := width - len(s)

    // Concatenate with the cached padding string.
    result := padding[needed] + s

    fmt.Println(result)
}

Pre-computing padding reduces allocation pressure. The make call creates the slice once. The loop fills it. Inside the hot path, you just index the slice and concatenate. This pattern pays off only at scale. For most applications, fmt.Sprintf or strings.Repeat is fast enough.

Pitfalls and errors

Padding code has a few common traps. The first is assuming fmt truncates long strings. It does not. If you pass a 20-character string to %10s, you get 20 characters in the output. The width is a minimum, not a maximum.

The second trap is passing the wrong type to fmt. Format verbs expect specific types. Using %d for a string causes a compile error. The compiler rejects this with cannot use "hello" (untyped string constant) as int value in argument. Always match the verb to the type.

The third trap is negative counts in strings.Repeat. If you calculate padding length without checking for overflow, a long string causes a negative count. strings.Repeat panics immediately. Always guard with a length check.

The fourth trap is forgetting that fmt width is byte-based. Multi-byte characters break visual alignment if you rely on len. Use utf8.RuneCountInString when character width matters.

Check the length before repeating. A negative count panics the program.

When to use what

Pick the tool that matches your padding character and performance needs.

Use fmt.Sprintf with width specifiers when you need space or zero padding and the string fits within the target width.

Use fmt.Sprintf with the * flag when the width is determined at runtime and you want to avoid building format strings manually.

Use strings.Repeat combined with concatenation when you need a custom padding character like a dot or hash.

Use a helper function that checks length before repeating when you must handle strings that might exceed the target width without truncation.

Use utf8.RuneCountInString when your strings contain multi-byte characters and you need visual alignment based on character count.

Use pre-computed padding tables when you are padding millions of strings in a tight loop and allocation overhead matters.

Pick the tool that matches your padding character. fmt for spaces and zeros. strings for everything else.

Where to go next