Aus Linux-Magazin 02/2026

Mit Go den Kontoverlauf modelliert darstellen

© howtogoto / 123RF.com

Damit sein Girokonto nicht ins Minus läuft, sagt bei Mike Schilli ein Go-Programm zukünftige Saldi voraus und erlaubt minimale Pegelstände zwecks Zinsmaximierung.

Bis vor ein paar Jahren zahlten Banken auf Spareinlagen dermaßen niedrige Zinsen, dass es sich fast nicht rentierte, kurzzeitige Überschüsse überhaupt anzulegen. Nun geben Geldinstitute aber plötzlich 2 , 3 Prozent oder auf Wertpapiere sogar 5 Prozent Zinsen. Da erscheint es schier frevelhaft, unnötigerweise 5000 Euro als Puffer auf dem Girokonto zu belassen. Immerhin wüchsen fest angelegt daraus Zinsen, von denen sich dauerhaft ein Netflix- und ein Spotify-Abo finanzieren ließe.

Abbildung 1: Mike Schilli weiß seinen Geldspeicher ausreichend gefüllt.

Abbildung 1: Mike Schilli weiß seinen Geldspeicher ausreichend gefüllt.

Die Herausforderung dabei: Der Saldo des Girokontos darf nie ins Negative abgleiten, denn sonst verlangt die Bank horrende Schuldzinsen oder verweigert eine Buchung gänzlich. Die Kunst besteht also darin, zu einem aktuellen Saldo abschätzen zu können, wie lange er zukünftigen Abbuchungen standhält. Zwischenzeitlich landet auch wieder Gehalt auf dem Konto. Abbuchungen trudeln oft an festen Tagen im Monat ein. So geht die Miete meist am Monatsende ab. Kreditkarten (also echte Chargecards, keine Debitkarten) rechnet das Karteninstitut monatlich an einem festen Tag ab.

Abbildung 2: Zukünftige Buchungen auf dem Girokonto.

Abbildung 2: Zukünftige Buchungen auf dem Girokonto.

Mit Einzahlungen und Abbuchungen zu festen Zeiten im Monat geht es also nicht darum zu gewährleisten, dass alle Ausgaben jederzeit gedeckt sind. Der wahre Zins-Maverick (Abbildung 1) stellt lediglich sicher, dass das Konto zum Zeitpunkt der Abbuchung die gewünschte Geldmenge führt. Was derweil in Kreditkarten gepuffert ist, muss erst später gedeckt sein. Abbildung 2 zeigt, dass sich die Kurve wegen zwischenzeitlicher Geldeingänge stets im Positiven bewegt.

CSV mit Zukunftsdaten

Als Input eines neuen Analysewerkzeugs dient die CSV-Datei in Listing 1. Die beschreibt ähnlich wie vom Bankinstitut heruntergeladene Buchungsdaten meine Kontobewegungen – nur dass sie statt in der Vergangenheit in der Zukunft liegen. Woher stammen die genauen Beträge? Die Miete fällt jeden Monat gleich aus, beim Strom gilt die Pauschale. Bei Kreditkarten steht normalerweise schon fast einen Monat im Voraus fest, welcher Betrag am Ende des Abrechnungszeitraums fällig ist. Übrigens sind sämtliche Beträge im Artikel rein fiktiv. Für 994 Dollar schafft man es in San Francisco nicht einmal, eine Hundehütte anzumieten.

Listing 1

data.csv

date,amount,comment
2025-11-16,4234.43,Start Balance
2025-11-29,-994.16,Rent
2025-11-24,-1712.39,Visa
2025-11-30,1378.50,Income
2025-12-03,-1490.15,Amex
2025-12-12,-329.96,Mastercard
2025-12-15,1378.50,Income
2025-12-31,-994.16,Rent

In den Reihen der CSV-Datei finden sich jeweils das Datum, der Buchungsbetrag (mit Minuszeichen vor Abbuchungen) und ein Kommentarfeld zur Identifizierung. Diese Zeilen in einer Skriptsprache einzulesen, wäre freilich ein Klacks, doch Go bereitet wegen seiner strengen Typen Mehrarbeit. Listing 2 zeigt, wie das geht, und dank des Fremdpakets »gocsv« von GitHub bleibt der Code dennoch schön kompakt.

Ein Datum im Format »2026-05-01«, also der 1. Mai 2026, steht in der CSV-Datei zum Beispiel als String, da CSV keine Typen kennt. Go will hingegen lieber eine Variable vom Typ »time.Time«, denn damit findet schon mal eine gewisse Werteprüfung statt. Den 32. Januar würde das »time«-Paket sofort zurückweisen. Zudem gehen spätere Datumsberechnungen leichter von der Hand.

Den Typ »Record« definiert Zeile 21 als Struktur mit drei Feldern, »Date«, »Amount« und »Comment«. Den Kommentar »Comment« legt Zeile 24 als »string« fest und weist den CSV-Parser mit »’csv:”comment”‘« darauf hin, dass die zugehörige Spalte in der Header-Zeile der CSV-Datei mit »comment« überschrieben ist.

Listing 2

csv.go

package main
import (
  "os"
  "slices"
  "time"
  "github.com/gocarina/gocsv"
  "github.com/govalues/decimal"
)
type Date struct{ time.Time }
func (d *Date) UnmarshalCSV(s string) error {
  t, err := time.Parse("2006-01-02", s)
  d.Time = t
  return err
}
type Decimal struct{ decimal.Decimal }
func (d *Decimal) UnmarshalCSV(s string) error {
  v, err := decimal.Parse(s)
  d.Decimal = v
  return err
}
type Record struct {
  Date    Date    `csv:"date"`
  Amount  Decimal `csv:"amount"`
  Comment string  `csv:"comment"`
}
func FromStdin() ([]Record, error) {
  var recs []Record
  if err := gocsv.Unmarshal(os.Stdin, &recs); err != nil {
    return recs, nil
  }
  slices.SortFunc(recs, func(a, b Record) int {
    return a.Date.Time.Compare(b.Date.Time)
  })
  return recs, nil
}

Beim Datum und dem Buchungsbetrag »Amount« wird es schon kniffliger: Hier muss der CSV-Parser Strings in Go-Typen verwandeln. Wie das klappt, definiert die Funktion »UnmarshalCSV()« auf den jeweiligen Typ. Und da Programmierer nicht auf Standardtypen wie »time.Time« herumorgeln dürfen, wickelt Zeile 9 zum Beispiel letzteren in einen Custom-Typ namens »Date«. An diesen wiederum hängt Zeile 10 mit »UnmarshalCSV()« Instruktionen zum Entpacken der CSV-Daten. Der Zeit-Parser »time.Parse()« schnappt sich dort mit dem in Go üblichen kuriosen Formatstring »2006-01-02« ein Datum im Format “JJJJ-MM-TT” und speichert es in einer Variablen des Standardtyps »time.Time«.

Listing 3

floatnok.go

package main
import "fmt"
func main() {
  a := 1000.01
  b := 2000.02
  c := a + b
  fmt.Printf("a = %.17f\n", a)
  fmt.Printf("b = %.17f\n", b)
  fmt.Printf("a + b = %.17f\n", c)
}

Geld nicht als Fließkomma

Analoges gilt für die Buchungsbeträge in der zweiten Spalte der CSV-Datei. Geldbeträge sollte man bekanntlich nicht als Fließkommazahlen darstellen, unabhängig davon, welche Programmiersprache zum Einsatz kommt. Warum? Listing 3 zeigt, was passiert, wenn der Code die Geldbeträge »1000.01« und »2000.02« als in den in Go üblichen Float64-Typen speichert und addiert. Die entsprechende Ausgabe zeigt Listing 4, und die überrascht erfahrene Programmierer nicht.

Listing 4

Ausgabe

a = 1000.00999999999999091
b = 2000.01999999999998181
a + b = 3000.02999999999974534

Bekanntlich scheitern CPUs daran, krumme Euro- und Centbeträge nicht hundertprozentig genau in ihren Fließkommaregistern zu speichern. Wer Finanzdaten damit aufaddiert, vertut sich früher oder später um Centbeträge – und rauft sich die Haare. Dabei handelt es sich keineswegs um eine unschöne Angewohnheit von Go – in Python oder C tritt derselbe Effekt auf. Selbst Ebay rechnet nicht richtig und verhaut die monatliche Abrechnung regelmäßig um ein paar Cent [1].

Stattdessen arbeitet Finanzsoftware normalerweise mit Centbeträgen in Integer-Variablen oder zieht ein Paket wie »decimal« wie in Zeile 7 von Listing 2 zurate. Der Unmarshaler für die Geldbeträge ab Zeile 16 ruft »Parse()« auf das CSV-Feld auf und liefert eine Variable vom Typ »decimal.Decimal« zurück, der hundertprozentig genaue Finanzarithmetik beherrscht.

Die Daten in der CSV-Datei müssen nicht unbedingt in der korrekten zeitlichen Reihenfolge stehen. Und so sortiert die Funktion »SortFunc()« in Zeile 31 von Listing 2 die geparsten Ergebnisse, damit spätere Stationen in der Pipeline sich leichter beim Aufsummieren tun. Die Sortierfunktion »SortFunc()« ist relativ neu: Sie erblickte mit Go 1.21 (August 2023) in dem neuen Paket »slices« das Licht der Welt. Vorher gab es »sort.Slice()« mit leicht abweichender Semantik.

Listing 5

balance.go

package main
import (
  "fmt"
  "github.com/govalues/decimal"
)
func main() {
  recs, err := FromStdin()
  if err != nil {
    panic(err)
  }
  fmt.Printf("date,amount,comment\n")
  total, _ := decimal.Parse("0.00")
  for _, r := range recs {
    total, _ = total.Add(r.Amount.Decimal)
    fmt.Printf("%s,%s,%s\n", r.Date.Format("2006-01-02"), total, r.Comment)
  }
}

Simple Arithmetik

Simple Arithmetik mit den Geldbeträgen sehen Sie in Listing 5. Es liest die CSV-Daten mit den Buchungen mittels der Funktion »FromStdin()« aus Listing 2 ein und gibt ebenfalls die CSV-Daten aus, nur mit der laufenden Summe statt mit den Einzelbuchungen von Listing 1.

Beim Aufsummieren der Geldbeträge hilft die Funktion »Add()« des Pakets »decimal«. Greift der Code daraufhin mit »r.Amount« in die »Record«-Struktur, steht dort ein Feld mit dem Custom-Type »Decimal« aus Zeile 15 in Listing 2. Der Custom-Type wiederum greift mit einem weiteren »Decimal« in die Wrapper-Struktur hinein, um zum eigentlichen Typ »Decimal« im Paket »decimal« zu gelangen. Gos Typsystem zeigt sich mitunter arg streng – da raucht einem der Kopf.

Wunderschönes Design

Am Ende der Pipeline »cat data.csv | ./balance« wartet nun ein aus Listing 6 kompiliertes Binary, das den Graphen in Abbildung 2 zeichnet. Es nutzt das Projekt »go-echarts« auf GitHub, eine Sammlung gängiger Chartformen. Von der Balkengrafik bis zum Tacho und 3D-Plots ist darin alles enthalten. Das Hauptaugenmerk des Projekts richtet sich auf formschönes Design. Dabei spuckt die Library keine PNG-Dateien aus wie andere Grafikpakete, sondern in eine HTML-Datei verpacktes JavaScript, das einen öffnenden Browser dazu veranlasst, den Graphen zu zeichnen.

Das Hauptprogramm »main« ab Zeile 47 in Listing 6 liest die auf Stdin hereinpurzelnden Datenhappen des CSV-Parsers ein und pumpt sie in den Array-Slice »pts«. Der wiederum enthält Strukturen vom Typ »pt«, die die Felder »Time« (Zeitpunkt der Buchung/des Saldos), »Value« (Geldbetrag) und »Comment« (Beschreibung der Buchung) führen.

Listing 6

chart.go

package main
import (
  "os"
  "time"
  "github.com/go-echarts/go-echarts/v2/charts"
  "github.com/go-echarts/go-echarts/v2/components"
  "github.com/go-echarts/go-echarts/v2/opts"
)
type Point struct {
  Time    time.Time
  Value   float64
  Comment string
}
func lineChart(pts []Point) *charts.Line {
  x := make([]string, len(pts))
  y := make([]opts.LineData, len(pts))
  for i, p := range pts {
    x[i] = p.Time.Format("2006-01-02")
    // Name = label text
    y[i] = opts.LineData{
      Name:  p.Comment,
      Value: p.Value,
    }
  }
  chart := charts.NewLine()
  chart.SetGlobalOptions(
    charts.WithTitleOpts(opts.Title{
      Title: "Checking Account",
    }),
    charts.WithXAxisOpts(opts.XAxis{
      Type: "category",
    }),
    charts.WithYAxisOpts(opts.YAxis{}),
  )
  chart.SetXAxis(x).AddSeries("Balance", y,
    charts.WithLineChartOpts(opts.LineChart{
      ShowSymbol: opts.Bool(true),
    }),
    charts.WithLabelOpts(opts.Label{
      Show:      opts.Bool(true),
      Position:  "left",
      Formatter: "\n{b}    ", // displays the Name field
    }),
  )
  return chart
}
func main() {
  recs, err := FromStdin()
  if err != nil {
    panic(err)
  }
  pts := []Point{}
  for _, r := range recs {
    val, _ := r.Amount.Decimal.Float64()
    pt := Point{
      Time:    r.Date.Time,
      Value:   val,
      Comment: r.Comment,
    }
    pts = append(pts, pt)
  }
  page := components.NewPage()
  page.AddCharts(lineChart(pts))
  f, err := os.Create("chart.html")
  if err != nil {
    panic(err)
  }
  defer f.Close()
  page.Render(f)
}

In das Graphen-Areal der erzeugten HTML-Seite wandert der X/Y-Plot mit »AddCharts()« in Zeile 63. Die Funktion zieht ihrerseits »lineChart()« ab Zeile 14 heran, um die Feineinstellungen des Graphen wie Titel und Achsenbeschriftung festzulegen. Heraus kommt schließlich die Datei »chart.html«, die in einen Browser geladen den Graphen nach Abbildung 2 produziert. Dabei arbeiten das verwendete HTML und JavaScript selbstständig, nachdem der Browser die Bibliothek einmal vom GitHub-Server geladen hat.

Zum Bauen des Binarys »chart« muss wieder der bekannte Go-Dreisprung ran:

go mod init chart; go mod tidy; go build chart.go csv.go

Damit holt der Go-Compiler sämtliche Abhängigkeiten von GitHub herein und bündelt anschließend allen Code in einem Binary.

Retro-Kalender

Wer statt formschöner Grafik Terminal-Ästhetik im Retro-Look bevorzugt, den bedient Listing 7 mit der Utility »cal«, die Zeichenkalender im Stil der Unix-Shell malt. Das Paket »go-textcal« auf GitHub beherrscht nicht nur das Zeichnen der Tage und Wochen eines Monats, sondern markiert auch einzelne Tage im Ausgabetext farblich. Außer bestimmten Wochenzeilen fügt es sogar fußnotenartig Kommentare an.

Abbildung 3: Der Terminal-Kalender mit den Buchungen.

Abbildung 3: Der Terminal-Kalender mit den Buchungen.

Abbildung 4: Hier sind die aus den Buchungen resultierenden Saldi zu sehen.

Abbildung 4: Hier sind die aus den Buchungen resultierenden Saldi zu sehen.

Abbildung 3 veranschaulicht die Textausgabe in der Shell für den aktuellen Monat mit den aus der CSV-Datei gelesenen und rechts angefügten Buchungen. Abbildung 4 bezieht sich auf die Saldi. Rote Beträge gingen vom Konto ab, grüne markieren Einzahlungen. Dabei korrespondiert eine Fußnote rechts immer mit dem identisch eingefärbten Tag in der Woche links.

Listing 7

cal.go

package main
import (
  "fmt"
  "github.com/fatih/color"
  "github.com/govalues/decimal"
  "github.com/mschilli/go-textcal"
  "time"
)
func main() {
  recs, err := FromStdin()
  if err != nil {
    panic(err)
  }
  dayMap := map[time.Time]decimal.Decimal{}
  for _, r := range recs {
    v := dayMap[r.Date.Time]
    v, _ = v.Add(r.Amount.Decimal)
    dayMap[r.Date.Time] = v
  }
  start := time.Now()
  fmt.Print(calMonth(start, dayMap), "\n")
  fmt.Print(calMonth(start.AddDate(0, 1, 0), dayMap), "\n")
  fmt.Print(calMonth(start.AddDate(0, 2, 0), dayMap), "\n")
}
func calMonth(date time.Time, dayMap map[time.Time]decimal.Decimal) string {
  year, month, _ := date.Date()
  firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
  cal := textcal.New(date)
  makegreen := cal.ColorFormatter(color.FgGreen, color.Reset)
  makered := cal.ColorFormatter(color.FgRed, color.Reset)
  for d := firstDay; d.Month() == month; d = d.AddDate(0, 0, 1) {
    formatter := makered
    switch dayMap[d].Sign() {
    case 0:
      continue
    case 1:
      formatter = makegreen
    }
    cal.UseFormatter(d.Day(), formatter)
    cal.Annotate(d.Day(), formatter(dayMap[d].String()))
  }
  return cal.String()
}

Listing 7 holt das Kalenderpaket »go-textcal« von GitHub ab und muss sich deshalb nicht um die Details beim Erzeugen der Terminal-Ausgabe kümmern. In der »For«-Schleife ab Zeile 15 baut es sich eine Hashtabelle auf, die als Schlüssel Datumsstempel und als zugewiesene Werte »Record«-Strukturen entgegennimmt. Listing 2 hat sie aus den Zeilen der eingehenden Daten im CSV-Format produziert.

Da zu einem Datum mehrere Buchungen stattfinden können, holt die For-Schleife in Zeile 16 erst einmal den eventuell existierenden Tageswert ein. Dazu zählt sie dann mit »Add« aus dem Paket »decimal« den neuen Wert hinzu oder subtrahiert ihn, je nach Vorzeichen. Zeile 18 weist das Ergebnis wieder dem Eintrag in der Hashtabelle zu. In Skriptsprachen geht dergleichen in einer Zeile, aber Go besteht auf der ausführlichen Version. Doch es gibt eine Abkürzung: Besteht noch kein Eintrag in der Hashtabelle, liefert Zeile 16 den Nullwert des Typs, im vorliegenden Fall also eine auf null Geldeinheiten gesetzte Variable vom Typ »decimal«, die ein folgendes »Add()« klaglos ausführt.

Buntes im Terminal

Die Funktion »calMonth()« ab Zeile 25 zeichnet für die Textausgabe eines Monatskalenders verantwortlich. Sie erzeugt in Zeile 28 eine neue Kalenderstruktur und iteriert in der For-Schleife ab Zeile 31 durch die Tage des Monats. Die dazu notwendige Kalenderarithmetik – schließlich haben nicht alle Monate gleich viele Tage – hat Go in »time.Time« bereits eingebaut. So holt Zeile 26 zunächst Jahr und Monat des angegebenen Datums, dann setzt Zeile 27 den Tageszähler auf »1« und erhält so den ersten Tag des Monats als »time.Time«-Struktur. Die For-Schleife ab Zeile 31 zählt daraufhin in jedem Durchgang einen Tag weiter, bis sich der Monat im Datum in »d.Month()« ändert, bis also das Zählerdatum »d« im Folgemonat gelandet ist.

Wie die Library bestimmte Tage im Kalender hervorhebt, bestimmt der Aufruf der Funktion »UseFormater()« in Zeile 39. Der reicht eine Funktion an das Paket »textcal« durch, das beim Zeichnen des entsprechenden Tags später den Formatierer aufruft. Dabei handelt es sich um einen der beiden Formatierer »makegreen« oder »makered« aus den Zeilen 29 und 30. Sie färben einen ihnen übergebenen String mit ANSIColor-Farben ein und geben den so kodierten Text an den Aufrufer zurück.

Die Fußnoten rechts von den Kalenderkolumnen dagegen setzt der Aufruf der Funktion »Annotate()« in Zeile 40. Die eingangs erstellte Hashtabelle »dayMap« weist Kalendertagen im Integer-Format aufsummierte Buchungen des Tags zu und färbt den daraus entstehenden Text mit dem vorher gesetzten »formatter« rot oder grün.

Die eben erläuterte Funktion »calMonth« ab Zeile 25 zeichnet jeweils einen Monat. Da die Ausgabe in Abbildung 2 und Abbildung 3 jeweils die ersten drei Monate nach dem heutigen Datum anzeigt, rufen die Zeile 21 bis 23 die Funktion dreimal jeweils mit einem neuen Monat als Parameter auf.

Interessanterweise fängt in Amerika die Woche am Sonntag an, nicht wie in Europa am Montag. Deshalb nutzt auch der Retro-Kalender dieses Format – ganz wie die Unix-Versionen des Tools »cal«. Selbst heute zeigt die Manpage »man cal« ganz unten immer noch den Hinweis: “It is not possible to display Monday as the first day of the week with cal”. (uba)

Infos

  1. Michael Schilli, “Ladenhüter”: Linux-Magazin 06/16, S.86: http://www.linux-magazin.de/Ausgaben/2016/06/Perl-Snapshot
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben