fmt.Sprintln ist der String-Zwilling von fmt.Println: gleiche Formatierungsregeln, gleiche Operanden-Behandlung — nur landet das Ergebnis nicht auf os.Stdout, sondern als string in deinem Programm. Damit hast du eine Println-Zeile in der Hand, bevor sie irgendwo geschrieben wird.
Zwei Garantien prägen die Funktion: Zwischen je zwei Operanden steht immer ein Leerzeichen, am Ende steht immer ein \n. Das macht Sprintln zur ersten Wahl, wenn du zeilenweise Strings für Buffer, Test-Erwartungen oder Netzwerk-Protokolle brauchst.
Die Funktions-Signatur
func Sprintln(a ...any) string — nimmt beliebig viele Operanden entgegen und liefert genau einen String mit Trailing-Newline zurück. Es gibt keinen Format-String und keine Fehler-Rückgabe — die Funktion kann nicht scheitern, weil sie nur in den Heap schreibt. Das variadische a ...any akzeptiert eine beliebige Mischung aus Typen; intern wendet Sprintln auf jeden Operanden das %v-Verb an und prüft optional auf das fmt.Stringer-Interface.
Spaces, Newline, Reihenfolge
Sprintln ist deterministisch: Die Operanden werden in der angegebenen Reihenfolge ausgewertet, mit Leerzeichen verbunden und mit \n abgeschlossen. Anders als bei fmt.Sprint gilt die Space-Regel immer — egal ob beide Nachbarn Strings sind oder nicht.
package main
import "fmt"
func main() {
s1 := fmt.Sprintln("Server", "started", "on", "port", 8080)
s2 := fmt.Sprintln("a", "b", "c")
s3 := fmt.Sprintln(1, 2, 3)
s4 := fmt.Sprintln()
fmt.Printf("%q\n", s1)
fmt.Printf("%q\n", s2)
fmt.Printf("%q\n", s3)
fmt.Printf("%q\n", s4)
}"Server started on port 8080\n"
"a b c\n"
"1 2 3\n"
"\n"Beachte den letzten Fall: Selbst ohne Operanden liefert Sprintln einen nicht-leeren String — nämlich nur "\n". Wer also „leere Strings" als Marker erwartet, wird hier überrascht. Wenn du einen wirklich leeren String brauchst, ist fmt.Sprint() die richtige Wahl.
Sprint vs. Sprintln vs. Sprintf
| Funktion | Eingabe | Ausgabe (mit %q) | Spaces | Trailing-\n |
|---|---|---|---|---|
fmt.Sprint("a", "b", 3) | drei Operanden | "ab 3" | nur zwischen Nicht-Strings | nein |
fmt.Sprintln("a", "b", 3) | drei Operanden | "a b 3\n" | immer | ja |
fmt.Sprintf("%s-%s-%d", "a", "b", 3) | Format + Args | "a-b-3" | gemäß Format | nein |
Faustregel: Sprintf braucht eine Schablone, Sprint und Sprintln brauchen keine — und unter den beiden Letzten entscheidet die Frage „brauche ich eine Zeile?" über die Wahl.
Wo Sprintln glänzt
Überall, wo zeilenbasierte Strings entstehen und du sie nicht sofort schreibst, ist Sprintln passend: Buffer und Builder mit Zeilen füllen, ohne manuell \n anzuhängen; Log-Zeilen bauen, bevor sie an mehrere Sinks (File, Network, Stdout) verteilt werden; zeilenbasierte Netzwerk-Protokolle wie SMTP, IMAP, HTTP/1.x; Test-Erwartungen, wenn der Code-Under-Test Println verwendet und im Test exakt derselbe String erwartet wird.
package main
import "fmt"
func main() {
helo := fmt.Sprintln("HELO", "mail.example.com")
mailFrom := fmt.Sprintln("MAIL", "FROM:<alice@example.com>")
fmt.Printf("%q\n", helo)
fmt.Printf("%q\n", mailFrom)
}"HELO mail.example.com\n"
"MAIL FROM:<alice@example.com>\n"Für echtes SMTP würdest du \r\n statt \n brauchen — dafür ist Sprintln dann nicht das richtige Werkzeug. Aber für Protokolle mit reinem \n-Trenner spart Sprintln eine Fehlerquelle: das vergessene Newline am Zeilenende.
Allokation und Reflection
Sprintln ist nicht für Hot-Paths gemacht. Drei Kostenfaktoren fallen ins Gewicht: Reflection für jeden Operanden über reflect; Heap-Allokation für den Rückgabe-String (immer neu); und Interface-Boxing für primitive Werte wie int, die in ein any-Interface verpackt werden. In einem Hot-Loop ist strconv plus strings.Builder deutlich schneller. Sprintln ist die richtige Wahl, wenn Klarheit und Vollständigkeit (Spaces, Newline) wichtiger sind als jede Mikrosekunde.
Die \n ist immer dabei
Die garantierte Trailing-Newline ist der entscheidende Unterschied zu Sprint und Sprintf. Das macht Sprintln großartig für zeilenbasierte Senken — und gefährlich, wenn du den String später nochmal einrahmst.
package main
import "fmt"
func main() {
line := fmt.Sprintln("Antwort:", 42)
fmt.Printf("%s\n", line)
fmt.Print(line)
}Antwort: 42
Antwort: 42Im ersten Fall steht eine Leerzeile zwischen den beiden Ausgaben, weil %s\n an die bereits vorhandene Sprintln-Newline noch eine zweite hängt. Merksatz: Wer Sprintln verwendet, sollte den Wert mit fmt.Print oder direkt mit io.WriteString ausgeben — nicht mit Println oder %s\n.
Buffer-Aufbau mit garantierten Zeilen
Ein häufiges Muster: Mehrere Datenpunkte werden zu einem mehrzeiligen String zusammengesetzt — etwa für einen Diagnose-Bericht, der später als Ganzes geschrieben wird. Sprintln nimmt dir die Newline-Disziplin ab.
package main
import (
"fmt"
"strings"
)
type Sensor struct {
Name string
Temperatur float64
Luftdruck int
}
func bericht(sensoren []Sensor) string {
var b strings.Builder
b.WriteString(fmt.Sprintln("=== Sensor-Bericht ==="))
for _, s := range sensoren {
b.WriteString(fmt.Sprintln(s.Name, s.Temperatur, "°C", s.Luftdruck, "hPa"))
}
b.WriteString(fmt.Sprintln("=== Ende ==="))
return b.String()
}
func main() {
daten := []Sensor{
{"Halle-Nord", 21.4, 1013},
{"Halle-Süd", 22.1, 1012},
}
fmt.Print(bericht(daten))
}=== Sensor-Bericht ===
Halle-Nord 21.4 °C 1013 hPa
Halle-Süd 22.1 °C 1012 hPa
=== Ende ===Kein einziges \n im Code — und trotzdem stimmen alle Zeilenumbrüche. Die Ausgabe mit fmt.Print (nicht Println) am Ende verhindert die Doppel-Newline.
Mock-Email-Body für Tests
In Tests von HTTP-Clients oder Parsern brauchst du oft einen kontrollierten Response-Body mit präzisen Zeilen. Sprintln baut diese Bodies lesbar auf — und die Newlines sind exakt dort, wo der echte Server sie setzen würde.
package mailparser_test
import (
"fmt"
"strings"
"testing"
)
func mockEmailBody() string {
var b strings.Builder
b.WriteString(fmt.Sprintln("From:", "alice@example.com"))
b.WriteString(fmt.Sprintln("To:", "bob@example.com"))
b.WriteString(fmt.Sprintln("Subject:", "Statusmeldung"))
b.WriteString(fmt.Sprintln())
b.WriteString(fmt.Sprintln("Hallo Bob,"))
b.WriteString(fmt.Sprintln("der Build ist grün."))
return b.String()
}
func TestParseHeaders(t *testing.T) {
body := mockEmailBody()
if !strings.Contains(body, "Subject: Statusmeldung\n") {
t.Fatalf("Subject-Zeile fehlt — Body war:\n%s", body)
}
if !strings.Contains(body, "\n\n") {
t.Fatal("Leerzeile zwischen Headern und Body fehlt")
}
}Die Leerzeile zwischen Headern und Body — bei E-Mails RFC-relevant — entsteht durch fmt.Sprintln() ohne Argumente und ergibt den String "\n". Zusammen mit der Trailing-Newline der vorherigen Zeile wird daraus die geforderte Doppel-Newline "\n\n".
Interessantes
Auto-Spaces zwischen ALLEN Operanden
Anders als fmt.Sprint setzt Sprintln Leerzeichen zwischen jedem Paar von Operanden — unabhängig vom Typ.
Auto-Newline am Ende
Das Ergebnis endet immer auf \n. Selbst fmt.Sprintln() ohne Argumente liefert den nicht-leeren String "\n".
Liefert String, schreibt nichts
Sprintln berührt weder os.Stdout noch einen io.Writer. Das Ergebnis wandert in den Heap und ist deins.
%v auf jedem Operanden
Intern wird jeder Operand mit dem Default-Format %v formatiert. Für Pretty-Printing mit Feldnamen brauchst du %+v, also Sprintf.
Stringer-Interface greift
Implementiert ein Typ fmt.Stringer, nutzt Sprintln diese Repräsentation. Damit bekommst du sauberes Logging ohne explizite %s-Verben.
Performance: nicht für Hot-Paths
Reflection, Interface-Boxing und garantierte Heap-Allokation summieren sich. In Hot-Loops ist strings.Builder plus strconv deutlich schneller.
Trailing-Newline-Falle
fmt.Printf("%s\n", sprintlnResult) ergibt eine Leerzeile zu viel. Sprintln-Ergebnisse gibst du mit fmt.Print aus.
In Tests: präzise Erwartungswerte
Wenn der Production-Code Println verwendet, erzeugt Sprintln im Test denselben String bit-genau. Das macht Vergleiche zuverlässig.