Ein Format-String in fmt ist mehr als nur das Verb. Zwischen dem % und dem Buchstaben am Ende steht ein kleines Mini-DSL, das Breite, Präzision, Ausrichtung und Vorzeichendarstellung steuert. Die vollständige Grammatik lautet %[flags][width][.precision]verb — alles zwischen % und Verb ist optional, doch wenn es da ist, steht es in genau dieser Reihenfolge.
Diese Modifikatoren sind der Unterschied zwischen einer zerfledderten Konsolenausgabe und einer sauber ausgerichteten Tabelle. Wer mit Go ernsthafte CLI-Werkzeuge schreibt, verbringt mehr Zeit mit %-12s und %8.2f als mit komplexer Geschäftslogik.
Die Grammatik im Detail
Der Aufbau folgt einer festen Reihenfolge: zuerst das Prozentzeichen, dann optional ein oder mehrere Flags, dann optional eine Width (Mindestbreite als Zahl), dann optional ein Punkt mit nachfolgender Precision, und schließlich zwingend ein Verb. Jedes Element ist für sich optional, aber die Reihenfolge ist fix. Auch der Punkt vor der Precision ist zwingend — %82f ist nicht dasselbe wie %8.2f.
package main
import "fmt"
func main() {
fmt.Printf("|%d|\n", 42)
fmt.Printf("|%5d|\n", 42)
fmt.Printf("|%.2f|\n", 3.14159)
fmt.Printf("|%8.2f|\n", 3.14159)
fmt.Printf("|%-5d|\n", 42)
fmt.Printf("|%-8.2f|\n", 3.14159)
}|42|
| 42|
|3.14|
| 3.14|
|42 |
|3.14 |Die Pipes | am Rand sind ein Trick, um die Leerzeichen am Anfang oder Ende sichtbar zu machen.
Width — Mindestbreite
Die Width ist eine Zahl direkt vor dem Verb (oder vor dem Punkt der Precision) und gibt die minimale Breite des formatierten Werts an. Ist der Wert kürzer, wird mit Leerzeichen aufgefüllt; ist er länger, wird nicht abgeschnitten. Die Default-Ausrichtung ist rechtsbündig.
package main
import "fmt"
func main() {
fmt.Printf("|%5d|\n", 1)
fmt.Printf("|%5d|\n", 42)
fmt.Printf("|%5d|\n", 12345)
fmt.Printf("|%5d|\n", 1234567)
fmt.Printf("|%10s|\n", "Go")
fmt.Printf("|%10s|\n", "Golang")
}| 1|
| 42|
|12345|
|1234567|
| Go|
| Golang|Beachte die vierte Zeile: 1234567 hat 7 Stellen, die Width war 5 — Go bricht hier nicht ab und kürzt auch nichts. Wer eine harte Obergrenze braucht, muss zur Precision greifen.
Precision — Nachkommastellen oder Max-Stringlänge
Die Precision steht hinter einem Punkt und hat zwei verschiedene Bedeutungen: Bei Floats die Anzahl der Nachkommastellen (%.2f rundet auf zwei Stellen). Bei Strings die maximale Anzahl Zeichen (%.5s schneidet nach 5 Zeichen ab). Bei Integern: minimale Anzahl Ziffern, aufgefüllt mit führenden Nullen.
package main
import "fmt"
func main() {
fmt.Printf("%.2f\n", 3.14159)
fmt.Printf("%.0f\n", 3.7)
fmt.Printf("%.6f\n", 1.0/3.0)
fmt.Printf("%.5s\n", "Hello, World")
fmt.Printf("%.3s\n", "Gopher")
fmt.Printf("%.5d\n", 42)
}3.14
4
0.333333
Hello
Gop
00042Bei Strings ist die Kürzung byte-basiert — Vorsicht bei Multibyte-Zeichen wie deutschen Umlauten, dort kann %.5s mitten in einer UTF-8-Sequenz schneiden.
Width + Precision kombiniert
Die Kombination %8.2f ist der Klassiker für Geldbeträge in Tabellen: 8 Zeichen Gesamtbreite, davon 2 Nachkommastellen. Damit stehen die Dezimalpunkte sauber untereinander.
package main
import "fmt"
func main() {
fmt.Printf("|%8.2f|\n", 3.14159)
fmt.Printf("|%8.2f|\n", 1234.5)
fmt.Printf("|%8.2f|\n", 0.1)
fmt.Printf("|%10.5s|\n", "Hello, World")
fmt.Printf("|%10.5s|\n", "Hi")
}| 3.14|
| 1234.50|
| 0.10|
| Hello|
| Hi|Die Dezimalpunkte in der Float-Spalte stehen exakt untereinander — genau das, was eine professionelle Tabellenausgabe braucht.
Flag -: Links-Alignment
Default ist rechtsbündig. Mit dem Flag - (Minus direkt nach dem %) wird linksbündig ausgerichtet, das Padding wandert ans Ende. Das ist die übliche Wahl für textuelle Spalten wie Namen oder Bezeichnungen.
package main
import "fmt"
func main() {
fmt.Printf("|%10s|\n", "Go")
fmt.Printf("|%-10s|\n", "Go")
fmt.Printf("|%-10s|\n", "Golang")
fmt.Printf("|%-10s|%5d|\n", "Apfel", 3)
fmt.Printf("|%-10s|%5d|\n", "Banane", 12)
fmt.Printf("|%-10s|%5d|\n", "Kirsche", 144)
}| Go|
|Go |
|Golang |
|Apfel | 3|
|Banane | 12|
|Kirsche | 144|- und 0 schließen sich gegenseitig aus — Zero-Padding ergibt bei Linksausrichtung keinen Sinn.
Flag 0: Zero-Padding
Das Flag 0 füllt die Width nicht mit Leerzeichen, sondern mit führenden Nullen auf. Sinnvoll ausschließlich für numerische Verben. Klassischer Anwendungsfall: Zeitanzeigen wie 09:05:03, Hex-Werte mit fester Breite (0x00ff), ID-Strings mit Padding (00042).
package main
import "fmt"
func main() {
fmt.Printf("%05d\n", 42)
fmt.Printf("%05d\n", 12345)
h, m, s := 9, 5, 3
fmt.Printf("%02d:%02d:%02d\n", h, m, s)
fmt.Printf("0x%04x\n", 255)
fmt.Printf("%08.2f\n", 3.14)
}00042
12345
09:05:03
0x00ff
00003.14Das %02d-Muster ist eines der häufigsten überhaupt in Go-Code — überall, wo Uhrzeiten, Datumsteile oder fortlaufende IDs formatiert werden.
Flag +: Vorzeichen erzwingen
Default schreibt Go ein Minuszeichen vor negative Zahlen, aber kein Pluszeichen vor positive. Mit dem Flag + wird das Vorzeichen immer ausgegeben. Das ist nützlich in Tabellen mit gemischten Vorzeichen.
package main
import "fmt"
func main() {
fmt.Printf("%+d\n", 42)
fmt.Printf("%+d\n", -7)
fmt.Printf("%+d\n", 0)
deltas := []int{3, -5, 12, 0, -1}
for _, d := range deltas {
fmt.Printf("Delta: %+4d\n", d)
}
}+42
-7
+0
Delta: +3
Delta: -5
Delta: +12
Delta: +0
Delta: -1In der Delta-Tabelle stehen die Vorzeichen exakt untereinander, was den optischen Unterschied zwischen Zunahme und Abnahme sofort erkennbar macht.
Flag #: Alternate-Form
Das #-Flag aktiviert eine sogenannte „alternative" Darstellung, deren Bedeutung verb-abhängig ist. Bei Hex (%#x) wird das 0x-Präfix vorangestellt, bei Oktal (%#o) eine führende 0. Beim v-Verb (%#v) liefert es die Go-Syntax-Repräsentation.
package main
import "fmt"
func main() {
fmt.Printf("%x\n", 255)
fmt.Printf("%#x\n", 255)
fmt.Printf("%o\n", 8)
fmt.Printf("%#o\n", 8)
type Punkt struct{ X, Y int }
p := Punkt{3, 4}
fmt.Printf("%v\n", p)
fmt.Printf("%+v\n", p)
fmt.Printf("%#v\n", p)
}ff
0xff
10
010
{3 4}
{X:3 Y:4}
main.Punkt{X:3, Y:4}Die drei v-Varianten zeigen die Eskalationsstufen: knapp, mit Feldnamen, mit Typ und Go-Syntax. Für Logging in der Entwicklung ist %#v Gold wert.
Dynamische Width mit *
Wenn die Spaltenbreite erst zur Laufzeit feststeht — etwa weil man die längste Bezeichnung in einer Liste ermittelt hat —, kann man die Width nicht hartcodieren. Hier hilft das *: Es nimmt die Width-Angabe nicht aus dem Format-String, sondern aus einem Argument.
package main
import "fmt"
func main() {
width := 10
fmt.Printf("|%*d|\n", width, 42)
fmt.Printf("|%-*s|\n", width, "Go")
digits := 3
fmt.Printf("%.*f\n", digits, 3.14159)
fmt.Printf("|%*.*f|\n", 10, 2, 3.14159)
namen := []string{"Apfel", "Brombeere", "Kirsche"}
maxLen := 0
for _, n := range namen {
if len(n) > maxLen {
maxLen = len(n)
}
}
for _, n := range namen {
fmt.Printf("|%-*s|\n", maxLen, n)
}
}| 42|
|Go |
3.142
| 3.14|
|Apfel |
|Brombeere|
|Kirsche |Das Muster maxLen ermitteln, dann mit %-*s ausrichten — die Standard-Technik für jede dynamische Tabellenausgabe in Go.
Tabellen-Ausgabe mit gemischten Spalten
Eine typische Aufgabe: eine Produktliste mit Name, Preis und Menge so ausgeben, dass die Spalten ausgerichtet sind.
package main
import "fmt"
type Produkt struct {
Name string
Preis float64
Menge int
}
func main() {
produkte := []Produkt{
{"Apfel", 0.99, 42},
{"Brombeere", 3.5, 7},
{"Kirsche", 4.25, 120},
{"Wassermelone", 8.9, 3},
}
fmt.Printf("%-12s %8s %5s\n", "Name", "Preis", "Menge")
fmt.Println("------------ -------- -----")
for _, p := range produkte {
fmt.Printf("%-12s %8.2f %5d\n", p.Name, p.Preis, p.Menge)
}
}Name Preis Menge
------------ -------- -----
Apfel 0.99 42
Brombeere 3.50 7
Kirsche 4.25 120
Wassermelone 8.90 3Die Dezimalpunkte stehen exakt untereinander. Für komplexere Tabellen lohnt sich der Wechsel zu text/tabwriter.
Progress-Bar mit dynamischer Breite
Ein klassischer Trick: eine Fortschrittsanzeige [####------] ohne Bibliothek bauen.
package main
import (
"fmt"
"strings"
)
func progress(percent, width int) string {
filled := width * percent / 100
bar := strings.Repeat("#", filled)
return fmt.Sprintf("[%-*s] %3d%%", width, bar, percent)
}
func main() {
for _, p := range []int{0, 25, 50, 75, 100} {
fmt.Println(progress(p, 30))
}
}[ ] 0%
[####### ] 25%
[############### ] 50%
[###################### ] 75%
[##############################] 100%Der Kern ist %-*s: linksbündig (-), Width aus Argument (*), String-Wert (s). Das Leerzeichen-Padding rechts vom #-Block ergibt die noch offene Strecke der Bar.
Interessantes
Reihenfolge ist fix
Die Grammatik %[flags][width][.precision]verb ist nicht verhandelbar — Width vor Precision, Punkt vor Precision-Zahl, Verb immer am Ende.
Width ist Mindestbreite
%5d reserviert mindestens 5 Zeichen; längere Werte werden nicht abgeschnitten. Für harte Obergrenzen bei Strings braucht es Precision.
Precision ist mehrdeutig
Bei Floats Nachkommastellen, bei Strings Maximalbreite, bei Integern Mindestziffern. Immer den Verb-Typ im Kopf behalten.
Default rechtsbündig
Ohne Flag richtet fmt rechtsbündig aus. Mit - linksbündig — typisch für Textspalten wie Namen.
Zero-Padding nur für Zahlen
0 füllt mit führenden Nullen — sinnvoll bei Uhrzeiten, IDs, Hex-Werten. - und 0 zusammen: 0 wird ignoriert.
+ erzwingt Vorzeichen
%+d zeigt + auch bei positiven Zahlen — ideal für Delta-Spalten mit gemischten Vorzeichen.
# ist verb-abhängig
%#x → 0x-Präfix, %#o → führende 0, %#v → Go-Syntax. Ein Flag, viele Bedeutungen.
* für dynamische Width
%*d und %.*f nehmen Width oder Precision aus einem Argument — Pflicht für generische Tabellenausgaben zur Laufzeit.