strconv.ParseFloat ist der zentrale Einstiegspunkt, wenn aus textueller Eingabe — sei es eine CSV-Zelle, ein HTTP-Query-Parameter oder ein JSON-Number-Token — ein numerischer Gleitkommawert werden soll. Die Funktion deckt nicht nur klassische Dezimalbrüche und die wissenschaftliche Notation ab, sondern versteht auch Hex-Floats nach C99-Manier sowie die IEEE-754-Spezialwerte NaN, +Inf und -Inf. Damit ist sie der robusteste String-zu-Float-Parser, den die Standardbibliothek anbietet.
Eine häufige Falle steckt im Parameter bitSize: er ist keine Range-Beschränkung, sondern eine Rundungsdirektive. Die Rückgabe ist immer ein float64, doch bei bitSize == 32 rundet ParseFloat so, dass der Wert ohne weiteren Präzisionsverlust in ein float32 konvertiert werden kann. Diese Unterscheidung — zusammen mit dem Verhalten bei ErrRange (±Inf bzw. ±0 statt Abbruch) — entscheidet darüber, ob produktiver Code numerisch korrekt bleibt.
Die Signatur ist schlank, aber jeder Parameter trägt Bedeutung. s ist der zu parsende String, bitSize ist 32 oder 64 — jeder andere Wert ist ein Programmierfehler und wird laut Dokumentation nicht unterstützt. Der zweite Rückgabewert ist ein *strconv.NumError, das den Funktionsnamen, die Originaleingabe und den unterliegenden Fehler (ErrSyntax oder ErrRange) bündelt.
Wichtig: anders als naive Eigenparser ist ParseFloat streng. Führende oder folgende Leerzeichen werden nicht toleriert — wer aus Textdateien liest, muss vorher strings.TrimSpace aufrufen. Kommata als Dezimaltrenner (deutsche Schreibweise) werden ebenfalls nicht akzeptiert; hier braucht es eine vorgelagerte Normalisierung.
package main
import (
"fmt"
"strconv"
)
func main() {
// func ParseFloat(s string, bitSize int) (float64, error)
v, err := strconv.ParseFloat("3.14159", 64)
fmt.Printf("v=%v err=%v\n", v, err)
// Strenge Eingabe: Leerzeichen führen zu ErrSyntax
_, err = strconv.ParseFloat(" 3.14 ", 64)
fmt.Println("mit Leerzeichen:", err)
}v=3.14159 err=<nil>
mit Leerzeichen: strconv.ParseFloat: parsing " 3.14 ": invalid syntaxDer NumError ist ein vollwertiges Error-Objekt mit Feldern Func, Num und Err. In Logs lohnt es sich, ihn via errors.As zu inspizieren, um die ursprüngliche Eingabe für die Diagnose zu erhalten.
ParseFloat versteht ein überraschend breites Spektrum. Neben einfachen Dezimalzahlen (123.45, .5, 5.) und Vorzeichen (+, -) gehört die wissenschaftliche Notation (1.5e10, 2E-7) zum Repertoire. Seit Go 1.13 kommen Hex-Floats im C99-Stil hinzu: 0x1.fp10 bedeutet 1.f (hexadezimal, also 1 + 15/16) mal 2^10. Der Exponent nach p ist dezimal und gibt eine Zweierpotenz an — ein häufiger Stolperstein.
Die Spezialwerte NaN, +Inf, -Inf und Infinity werden case-insensitiv erkannt. Das ist praktisch, wenn JSON-Dialekte oder Fremdsysteme diese Sonderfälle als Strings liefern.
package main
import (
"fmt"
"strconv"
)
func main() {
eingaben := []string{
"0", "3.14", ".5", "5.",
"1.5e10", "2E-7",
"0x1.fp10", // (1 + 15/16) * 2^10 = 1984
"0x1p-1", // 0.5
"NaN", "+Inf", "-infinity",
}
for _, s := range eingaben {
v, err := strconv.ParseFloat(s, 64)
fmt.Printf("%-12q -> %-12v err=%v\n", s, v, err)
}
}"0" -> 0 err=<nil>
"3.14" -> 3.14 err=<nil>
".5" -> 0.5 err=<nil>
"5." -> 5 err=<nil>
"1.5e10" -> 1.5e+10 err=<nil>
"2E-7" -> 2e-07 err=<nil>
"0x1.fp10" -> 1984 err=<nil>
"0x1p-1" -> 0.5 err=<nil>
"NaN" -> NaN err=<nil>
"+Inf" -> +Inf err=<nil>
"-infinity" -> -Inf err=<nil>Hex-Floats sind in numerisch sensitiven Domänen Gold wert: sie erlauben es, einen float64 bit-exakt zu serialisieren und wieder einzulesen, ohne Dezimal-Rundungsfehler einzuführen.
Das wohl meistmissverstandene Detail: bitSize schneidet nicht Wertebereiche ab. Bei bitSize == 32 wird der String so geparst, dass das Ergebnis dem entspricht, was man bei einer Konvertierung in float32 und zurück nach float64 erhielte. Der Rückgabetyp ist trotzdem float64 — die Konvertierung muss der Aufrufer selbst per float32(v) durchführen.
Das wird besonders sichtbar bei Zahlen, die in einfacher Präzision nicht exakt darstellbar sind. 0.1 ist in keiner binären Gleitkommarepräsentation exakt, aber die Abweichung in float32 ist deutlich größer als in float64.
package main
import (
"fmt"
"strconv"
)
func main() {
v32, _ := strconv.ParseFloat("0.1", 32)
v64, _ := strconv.ParseFloat("0.1", 64)
fmt.Printf("bitSize=32 -> %.20f\n", v32)
fmt.Printf("bitSize=64 -> %.20f\n", v64)
// Beweis: v32 entspricht der float32-Rundung
fmt.Printf("float32(v64) == v32? %v\n", float64(float32(v64)) == v32)
}bitSize=32 -> 0.10000000149011611938
bitSize=64 -> 0.10000000000000000555
float32(v64) == v32? trueFaustregel: wer am Ende einen float32 speichern will (etwa für Tensor-Daten oder Grafik-Buffer), sollte bitSize=32 verwenden, um die Rundungsfehler nicht doppelt einzusammeln.
Die Rundung folgt strikt IEEE 754 mit Round-Half-to-Even (Banker's Rounding). Bei mehrdeutigen Eingaben — etwa wenn der String genau zwischen zwei darstellbaren Float-Werten liegt — wird zur geraden Mantisse gerundet. Das verhindert systematische Bias in statistischen Aggregaten, kann aber bei Vergleichen mit handgerechneten Werten überraschen.
ParseFloat garantiert: der zurückgegebene float64 ist der nächstgelegene darstellbare Wert zur Eingabe. Es gibt keinen Modus für Truncation oder Round-Half-Up.
package main
import (
"fmt"
"strconv"
)
func main() {
// Klassisches Beispiel: 0.1 + 0.2 != 0.3
a, _ := strconv.ParseFloat("0.1", 64)
b, _ := strconv.ParseFloat("0.2", 64)
c, _ := strconv.ParseFloat("0.3", 64)
fmt.Printf("0.1+0.2 = %.20f\n", a+b)
fmt.Printf("0.3 = %.20f\n", c)
fmt.Println("gleich? ", a+b == c)
}0.1+0.2 = 0.30000000000000004441
0.3 = 0.29999999999999998890
gleich? falseDiese Eigenschaft ist kein Bug von ParseFloat, sondern ein fundamentales Merkmal binärer Gleitkommaarithmetik. Für finanzielle Berechnungen ist daher math/big.Float oder ein dediziertes Decimal-Paket die richtige Wahl.
Eine zentrale Eigenheit: bei Überschreitung des darstellbaren Bereichs gibt ParseFloat nicht Null zurück, sondern den nächstgelegenen Grenzwert — also ±math.MaxFloat64 plus Rundung zu ±Inf bei Overflow bzw. ±0 bei Underflow. Gleichzeitig wird ErrRange als Fehler geliefert. Wer den Fehler ignoriert und nur den Wert nutzt, bekommt also „etwas Sinnvolles" — aber eben mit verlorener Information.
package main
import (
"errors"
"fmt"
"strconv"
)
func main() {
v, err := strconv.ParseFloat("1e400", 64)
fmt.Printf("overflow: v=%v isRange=%v\n", v, errors.Is(err, strconv.ErrRange))
v, err = strconv.ParseFloat("1e-400", 64)
fmt.Printf("underflow: v=%v isRange=%v\n", v, errors.Is(err, strconv.ErrRange))
v, err = strconv.ParseFloat("1e40", 32)
fmt.Printf("float32 overflow: v=%v err=%v\n", v, err)
}overflow: v=+Inf isRange=true
underflow: v=0 isRange=true
float32 overflow: v=+Inf err=strconv.ParseFloat: parsing "1e40": value out of rangeIn Produktionscode immer mit errors.Is(err, strconv.ErrRange) prüfen — so lässt sich gezielt zwischen „Syntaxfehler" (verworfene Eingabe) und „zu groß/klein" (eventuell als ±Inf weiterverarbeitbar) unterscheiden.
Die wohl wichtigste Garantie für serialisierende Systeme: FormatFloat(v, 'g', -1, 64) erzeugt den kürzesten String, der via ParseFloat(..., 64) wieder exakt v ergibt. Diese Round-Trip-Eigenschaft (Ryū/Grisu-Algorithmus) ist verlustfrei und kompakter als naive %.17g-Formatierungen.
package main
import (
"fmt"
"math"
"strconv"
)
func main() {
werte := []float64{0.1, 1.0 / 3.0, math.Pi, 1e-300, math.MaxFloat64}
for _, v := range werte {
s := strconv.FormatFloat(v, 'g', -1, 64)
back, _ := strconv.ParseFloat(s, 64)
fmt.Printf("%-30q -> %v exact=%v\n", s, back, back == v)
}
}"0.1" -> 0.1 exact=true
"0.3333333333333333" -> 0.3333333333333333 exact=true
"3.141592653589793" -> 3.141592653589793 exact=true
"1e-300" -> 1e-300 exact=true
"1.7976931348623157e+308" -> 1.7976931348623157e+308 exact=trueWer beim Serialisieren prec=17 hartcodiert, verschwendet meist Bytes; wer prec=15 nimmt, riskiert Datenverlust. prec=-1 ist die einzig korrekte Wahl für verlustfreien Round-Trip.
Anders als strconv.ParseInt (das seit Go 1.13 Underscores in Number-Literalen toleriert, wenn base == 0) akzeptiert ParseFloat keine Unterstriche. Das verwirrt regelmäßig, weil Go-Quellcode 1_000_000.5 als Literal kennt — diese Schreibweise gilt jedoch nur für den Compiler, nicht für den Laufzeit-Parser.
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
_, err := strconv.ParseFloat("1_000.5", 64)
fmt.Println("direkt:", err)
s := "1_000_000.5"
bereinigt := strings.ReplaceAll(s, "_", "")
v, err := strconv.ParseFloat(bereinigt, 64)
fmt.Printf("nach Cleanup: v=%v err=%v\n", v, err)
}direkt: strconv.ParseFloat: parsing "1_000.5": invalid syntax
nach Cleanup: v=1.0000005e+06 err=<nil>Auch Tausendertrennzeichen (1,000.5 im US-Stil oder 1.000,5 im DE-Stil) sind tabu — solche lokalisierten Eingaben gehören in eine eigene Normalisierungsschicht, eventuell mit golang.org/x/text/number.
In CSV-Dateien lauern alle Probleme gleichzeitig: Leerzeichen, leere Zellen, lokalisierte Dezimaltrenner, Tausendertrenner. Eine produktionsreife Parse-Funktion isoliert diese Sonderfälle, bevor ParseFloat zum Zug kommt.
package main
import (
"encoding/csv"
"fmt"
"strconv"
"strings"
)
func parseFloatDE(raw string) (float64, error) {
s := strings.TrimSpace(raw)
if s == "" {
return 0, nil
}
// DE-Format: 1.234,56 -> 1234.56
s = strings.ReplaceAll(s, ".", "")
s = strings.ReplaceAll(s, ",", ".")
return strconv.ParseFloat(s, 64)
}
func main() {
raw := "name;preis\nApfel;1,29\nBirne; 2.499,50 \nKiwi;\nNektarine;abc\n"
r := csv.NewReader(strings.NewReader(raw))
r.Comma = ';'
rows, _ := r.ReadAll()
for _, row := range rows[1:] {
v, err := parseFloatDE(row[1])
fmt.Printf("%-10s -> %.2f (err=%v)\n", row[0], v, err)
}
}Apfel -> 1.29 (err=<nil>)
Birne -> 2499.50 (err=<nil>)
Kiwi -> 0.00 (err=<nil>)
Nektarine -> 0.00 (err=strconv.ParseFloat: parsing "abc": invalid syntax)Die Trennung zwischen Normalisierung und Parsing macht die Logik testbar: parseFloatDE lässt sich isoliert mit Tabellentests abdecken, während der CSV-Loop nur orchestriert.
encoding/json bietet mit json.Number einen String-basierten Number-Typ, der Präzisionsverluste vermeidet. Erst beim expliziten Float64()-Aufruf greift intern ParseFloat. Für große Aggregate (Statistik-Pipelines, Finanz-APIs) ist das Muster Pflicht.
package main
import (
"encoding/json"
"fmt"
"strings"
)
func main() {
raw := `{"betrag": 9999999999999999.99}`
dec := json.NewDecoder(strings.NewReader(raw))
dec.UseNumber()
var obj map[string]any
_ = dec.Decode(&obj)
if n, ok := obj["betrag"].(json.Number); ok {
v, err := n.Float64()
fmt.Printf("float64: %v (err=%v)\n", v, err)
fmt.Printf("original-string : %s\n", n.String())
}
}float64: 1e+16 (err=<nil>)
original-string : 9999999999999999.99Beachte den Präzisionsverlust bei betrag: 16 signifikante Dezimalstellen passen nicht mehr verlustfrei in einen float64. Wer den Originalwert braucht, muss auf json.Number.String() oder math/big.Float ausweichen — ParseFloat selbst kann hier nichts retten.
Interessantes
bitSize ist Rundung, nicht Range
bitSize=32 rundet auf float32-Präzision, gibt aber dennoch float64 zurück — kein Range-Check.
ErrRange ist nicht fatal
Bei Overflow liefert ParseFloat ±Inf, bei Underflow ±0 plus ErrRange — Wert ist nutzbar, Fehler aber prüfen.
Round-Trip nur mit prec=-1
FormatFloat(v, 'g', -1, 64) ist der einzige Weg zu verlustfreier String-Repräsentation.
Hex-Floats verfügbar
0x1.fp10 parst seit Go 1.13 — Exponent nach p ist dezimal, Basis ist 2.
NaN/Inf case-insensitiv
NaN, nan, +Inf, -infinity — alles akzeptiert.
Keine Underscores
Im Gegensatz zu ParseInt lehnt ParseFloat 1_000.5 als Syntaxfehler ab.
Keine Whitespace-Toleranz
Führende/folgende Leerzeichen führen zu ErrSyntax — vorher strings.TrimSpace.
Keine Lokalisierung
Dezimal-Komma und Tausenderpunkt müssen vorher normalisiert werden.