Aus Linux-Magazin 07/2025

Fotos suchen mit Go und Fyne

© Noppawan Laisuan / 123RF.com

Um ein bestimmtes Foto aufzuspüren, zieht Mike Schilli oft die Geo-Suchfunktion des Telefons zu Rate. Der Nachbau mit Go und Fyne funktioniert ohne Probleme.

In einer unaufgeräumten Fotosammlung findet sich ein gesuchter Schnappschuss häufig schneller auf einer Landkarte als in der Timeline. Die Foto-App des Handys zeigt hierzu auf der Karte Fotogrüppchen an (Abbildung 1) und offenbart auf einen Fingertipp hin eine Liste aller im Umkreis geschossenen Fotos. Wer weiß, woher eine Aufnahme stammt, landet so in vielen Fällen flott beim gesuchten Motiv.

Wir wollen ein Go-Programm bauen, das ähnliche Geo-Suchen leistet. Aber wo beginnen? Bei Tausenden von Fotos in einer tiefen Verzeichnishierarchie ergibt es wenig Sinn, sie bei jedem Aufruf der Applikation zu durchwandern und die Exif-Daten jedes einzelnen Fotos erneut zu extrahieren. Stattdessen spart es enorm Zeit, wenn die Metadaten aller Fotos schon im Speicher in einer Datenbank liegen. Listing 1 nutzt dazu eine SQLite-Datenbank, und dank Public-Domain-Status dieses achten Weltwunders der Open-Source-Welt darf das Go-Paket »go-slite3« auf Github gleich sämtlichen dazu notwendigen Code mit in eine Applikation verpacken.

Abbildung 1: Handy-Fotos entstehen nicht selten gehäuft an bestimmen Orten.

Abbildung 1: Handy-Fotos entstehen nicht selten gehäuft an bestimmen Orten.

Listing 1

index.go

 package main
 import (
   "database/sql"
   "fmt"
   "log"
   "os"
   "path/filepath"
   "strings"
   _ "github.com/mattn/go-sqlite3"
   "github.com/rwcarlsen/goexif/exif"
 )
 const dbFile = "photos.db"
 func main() {
   if len(os.Args) < 2 {
     log.Fatal("Usage: " + os.Args[0] + " /images")
   }
   rootDir := os.Args[1]
   db, err := sql.Open("sqlite3", dbFile)
   if err != nil {
     log.Fatal(err)
   }
   defer db.Close()
   createTable := `
     CREATE TABLE IF NOT EXISTS photos (
       path TEXT PRIMARY KEY,
       lat REAL,
       lon REAL
     );`
   if _, err := db.Exec(createTable); err != nil {
     log.Fatal("Can't create table:", err)
   }
   err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
     if err != nil || info.IsDir() {
       return nil
     }
     ext := strings.ToLower(filepath.Ext(path))
     if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
       return nil
     }
     return Save(db, path)
   })
   if err != nil {
     log.Fatal(err)
   }
 }
 func Save(db *sql.DB, path string) error {
   f, err := os.Open(path)
   if err != nil {
     return err
   }
   defer f.Close()
   x, err := exif.Decode(f)
   if err != nil {
     return err
   }
   lat, lon, err := x.LatLong()
   if err != nil {
     return err
   }
   _, err = db.Exec(`INSERT OR REPLACE INTO photos (path, lat, lon) VALUES (?, ?, ?)`, path, lat, lon)
   if err != nil {
     log.Printf("Can't insert %s: %v\n", path, err)
     return err
   }
   fmt.Printf("Inserted: %s -> (%f, %f)\n", path, lat, lon)
   return nil
 }

Das fertig gebaute Binary »index« erwartet als ersten Kommandozeilenparameter einen Verzeichnisbaum mit Fotos, durchläuft ihn mit »Walk« aus dem Paket »filepath« ab Zeile 32 und stöbert alle in beliebiger Tiefe darin enthaltenen JPEG-Fotos auf. Das Paket »goexif« auf Github extrahiert die in den Handy-Fotos gespeicherten Exif-Daten und die dort abgelegten GPS-Werte als geografischen Längen- und Breitengrad.

Falls die SQLite-Datenbank bis dato noch nicht existiert, richtet sie das SQL-Kommando ab Zeile 24 neu ein, mit den Spalten für den Pfad zum Foto und den Float-Werten für »lat« sowie »lon«. Auch der Insert-Befehl ab Zeile 60 kann mit bestehenden Daten umgehen. Dank »INSERT OR REPLACE« frischt die Engine die Werte bereits abgelegter Fotos allerhöchstens auf, trägt aber keine doppelten Zeilen ein. So lässt sich das Programm beliebig oft aufrufen – Idempotenz nennt das der Fachmann. Abbildung 2 zeigt die mit immerhin 3000 Fotos gefüllte SQLite-Datenbank nach Abschluss des Suchlaufs von »index«.

Abbildung 2: Die gef&uuml;llte Datenbank enth&auml;lt die Geokoordinaten aller Fotos.

Abbildung 2: Die gefüllte Datenbank enthält die Geokoordinaten aller Fotos.

Fotos nah und fern

Die fertige Applikation soll später wie in Abbildung 3 aussehen. Klickt der User auf einen Punkt der Landkarte, rechnet das Programm aus, auf welchen Geokoordinaten der gewählte Punkt liegt. Um herauszufinden, welche Fotos im Umkreis dieses Punkts entstanden sind, definiert Listing 2 die Funktion »findPhotosNear()« (Zeile 12). Sie durchläuft sämtliche in der Datenbank gespeicherten Fotos und meldet diejenigen, deren geografischer Abstand zum Referenzpunkt kleiner als der Suchradius »radiusKm« ist.

Abbildung 3: Die Optik der fertigen Fyne-Applikation.

Abbildung 3: Die Optik der fertigen Fyne-Applikation.

Listing 2

near.go

 package main
 import (
   "database/sql"
   geo "github.com/kellydunn/golang-geo"
   _ "github.com/mattn/go-sqlite3"
 )
 type Photo struct {
   Path string
   Lat  float64
   Lon  float64
 }
 func findPhotosNear(lat, lon float64, radius float64) ([]string, error) {
   db, err := sql.Open("sqlite3", "photos.db")
   if err != nil {
     return nil, err
   }
   defer db.Close()
   rows, err := db.Query(`SELECT path, lat, lon FROM photos`)
   if err != nil {
     return nil, err
   }
   defer rows.Close()
   results := []string{}
   for rows.Next() {
     var p Photo
     if err := rows.Scan(&p.Path, &p.Lat, &p.Lon); err != nil {
       return nil, err
     }
     p1 := geo.NewPoint(lat, lon)
     p2 := geo.NewPoint(p.Lat, p.Lon)
     if p1.GreatCircleDistance(p2) <= radius {
       results = append(results, p.Path)
     }
   }
   return results, nil
 }

Dazu öffnet die Funktion in Zeile 13 die vorher angelegte SQLite-Datenbank und iteriert mit dem SQL-Kommando »select« über alle Einträge in der Tabelle »photos«. Jede Tabellenzeile enthält die Geokoordinaten des betreffenden Fotos als Fließkommawerte. Fällt die Distanz dieses Geopunkts vom gewählten Referenzstandort kleiner aus als der eingestellte Suchradius, schafft es das Foto in die Auswahl.

Wie bestimmt sich nun die Distanz zweier Geopunkte auf der Erdkugel? Das ließe sich aufgrund der geringen Entfernung vereinfacht 2D-geometrisch lösen. Doch das Paket »golang-geo« auf Github verfügt dazu über die praktische Funktion »GreatCircleDistance()« (Zeile 31). Sie gibt die gesuchte Distanz zweier Punkte auf der 3D-Oberfläche der Erde einfach als Fließkommawert in Kilometern aus. Damit ist der Selektor von Bildern in einem festen Umkreis eines dynamisch gewählten Orts fertig. Anschließend geht es an die grafische Darstellung in einer Desktop-Anwendung.

Karte anklicken

Die GUI der Applikation zeigt später wie in Abbildung 3 eine Landkarte an, die Mausklicks oder Fingertipps entgegennimmt. Kartenmaterial bietet das OpenStreetMap-Projekt (OSM). Auf Abbildung 4 sehen Sie meine Wahlheimat San Francisco auf der OSM-Webseite im Browser. Das Projekt erlaubt aber nicht nur den Zugriff auf die Webanwendung, sondern offeriert das Kartenmaterial auch für selbstgeschriebene Desktop-Applikationen. Karten in verschiedenen Auflösungen – Zoom-Settings im OpenStreetMap-Jargon – liefern die sogenannten Tile-Server des Projekts als einzelne quadratische Ausschnitte. Diese Kacheln legt der Client daraufhin nahtlos aneinander, um die große Karte auf den Schirm zu bringen.

Abbildung 4: Das Openstreetmap-Projekt bietet frei verf&uuml;gbares Kartenmaterial.

Abbildung 4: Das Openstreetmap-Projekt bietet frei verfügbares Kartenmaterial.

Dank des auf Github zu findenden Pakets »go-staticmaps«, einem Klon des gleichnamigen Python-Projekts, klappt das Einholen der Tiles für eine vorgegebene Geo-Location recht flott (Listing 3). Weiter enthält das Paket eine Transformer-Funktion, die Klicks als X/Y-Koordinaten in Pixeln auf der lokal dargestellten Karte wieder in Längen- und Breitengrade auf der Erde zurückrechnet.

Allerdings funktioniert der in »go-staticmaps« eingebaute Transformator nicht ganz so wie erwartet. Gibt eine Anwendung eine Geokoordinate an, befindet sich die nur selten genau in der Mitte einer Kachel. In solchen Fällen sind die vom Transformator errechneten Längen- und Breitengrade verschoben, um den Offset innerhalb der Kachel. Das scheint ein Bug im Paket zu sein.

Als Workaround definiert Listing 3 im Typ »Smap« (Zeile 8) das Attribut »Trans«, das für den Client eine bereinigte Transformationsfunktion bereithält. Sie ermittelt die X/Y-Koordinaten auf der Kachel, auf die laut Original-Transformer die Kombination aus dem vorgegebenen Längen- und Breitengrad fällt. Aus der Differenz zur Bildmitte, definitionsgemäß auf halber Strecke zur Bildbreite und -höhe, rechnet die Funktion dann den Korrektur-Offset für X- und Y-Werte mit ein. Heraus kommen die richtigen Werte.

Listing 3

smap.go

 package main
 import (
   "github.com/flopp/go-staticmaps"
   "github.com/golang/geo/s2"
   "image"
   "os"
 )
 type Smap struct {
   Image image.Image
   Trans func(x, y float32) s2.LatLng
 }
 func NewSmap() *Smap {
   return &Smap{}
 }
 func (smap *Smap) Init() error {
   ctx := sm.NewContext()
   ctx.SetSize(MapWidth, MapHeight)
   ctx.SetUserAgent(AppName)
   cache := sm.NewTileCache("cache", os.ModePerm)
   ctx.SetCache(cache)
   center := s2.LatLngFromDegrees(CenterLat, CenterLon)
   ctx.SetCenter(center)
   ctx.SetZoom(Zoom)
   var err error
   smap.Image, err = ctx.Render()
   if err != nil {
     return err
   }
   trans, err := ctx.Transformer()
   if err != nil {
     return err
   }
   trueCenter := s2.LatLngFromDegrees(CenterLat, CenterLon)
   cx, cy := trans.LatLngToXY(trueCenter)
   smap.Trans = func(x, y float32) s2.LatLng {
     fx := cx - float64(MapWidth)/2 + float64(x)
     fy := cy - float64(MapHeight)/2 + float64(y)
     return trans.XYToLatLng(fx, fy)
   }
   return nil
 }

Wohl erzogen

Zwar stellt das OpenStreetMap-Projekt die Kartendaten kostenlos zur Verfügung, laut Webseite kann es aber nicht unbegrenzt Serverkapazitäten finanzieren. Darum limitiert der Service den Zugriff per User-Agent. Wenn Sie eine Standard-Go-Library mit »net/http« einsetzen, dürften Sie sich wundern, dass der darin intern verwendete User-Agent bereits geblockt wird. Listing 5 setzt ihn deshalb später explizit auf “Fyne Map Clicker 1.0”, und den behandelt OSM (noch) freundlich – jedenfalls bis Millionen von Lesern des Linux-Magazins den Code kopieren und auf OSM einhämmern.

Ohne Zutun holt sich das Paket »go-staticmaps« die Kachel des abgebildeten Landkartenausschnitts bei jedem Programmstart erneut vom Server. Das ist allerdings wenig empfehlenswert, denn es beansprucht nicht nur unnötig Server-Ressourcen, sondern sorgt auch dafür, dass die App bei fehlender Internetverbindung später nicht mehr funktioniert. Zeile 19 setzt deshalb den OpenStreetMap-Cache auf das Verzeichnis »”cache”« im aktuellen Verzeichnis. Das Verzeichnis legt das Paket, falls noch nicht vorhanden, selbständig an und speichert darunter neben der aktuell benötigten Kachel auch die angrenzenden. Das wissen Applikationen zu schätzen, die dem User das Herumschubsen der Landkarte erlauben.

Lagen-Look

Die GUI selbst besteht aus zwei Lagen, wie in Abbildung 5 skizziert. Das untere Stockwerk zeigt die Landkarte als Bild, obenauf liegt ein Layer, der Klicks entgegennimmt. So aktiviert, kramt er ein passendes Foto aus der Sammlung heraus und platziert es als Thumbnail an der geklickten Koordinate. Listing 4 implementiert das obere Widget mit dem grafischen Fyne-Framework in Go.

Abbildung 5: Unten der OpenStreetMap-Container, oben ein klickbares Canvas-Objekt mit Foto.

Abbildung 5: Unten der OpenStreetMap-Container, oben ein klickbares Canvas-Objekt mit Foto.

Listing 4

overlay.go

 package main
 import (
   "fyne.io/fyne/v2"
   "fyne.io/fyne/v2/canvas"
   "fyne.io/fyne/v2/container"
   "fyne.io/fyne/v2/widget"
   "github.com/disintegration/imaging"
   "golang.org/x/text/language"
   "golang.org/x/text/message"
   "image/color"
 )
 const (
   ThumbSize = 200
 )
 type tapOverlay struct {
   widget.BaseWidget
   con  *fyne.Container
   card *fyne.Container
   cb   func(fyne.Position)
 }
 func newTapOverlay() *tapOverlay {
   over := &tapOverlay{}
   over.ExtendBaseWidget(over)
   over.con = container.NewWithoutLayout()
   return over
 }
 func (t *tapOverlay) CreateRenderer() fyne.WidgetRenderer {
   return widget.NewSimpleRenderer(t.con)
 }
 func (t *tapOverlay) Tapped(pe *fyne.PointEvent) {
   t.cb(pe.Position)
 }
 func (t *tapOverlay) newCard(fileName string, total int) *fyne.Container {
   img, err := imaging.Open(fileName, imaging.AutoOrientation(true))
   if err != nil {
     panic(err)
   }
   thumbnail := imaging.Thumbnail(img, int(ThumbSize), int(ThumbSize), imaging.Lanczos)
   image := canvas.NewImageFromImage(thumbnail)
   image.FillMode = canvas.ImageFillContain
   image.SetMinSize(fyne.NewSize(ThumbSize, ThumbSize))
   p := message.NewPrinter(language.English)
   label := widget.NewLabel(p.Sprintf("%d", total))
   bg := canvas.NewRectangle(color.White)
   return container.NewVBox(image, container.NewMax(bg, label))
 }
 func (t *tapOverlay) ShowThumb(path string, total int, pos fyne.Position) {
   t.Clear()
   card := t.newCard(path, total)
   card.Move(pos)
   card.Resize(fyne.NewSize(ThumbSize, ThumbSize))
   t.con.Add(card)
   t.card = card
   canvas.Refresh(t)
 }
 func (t *tapOverlay) Clear() {
   if t.card != nil {
     t.con.Remove(t.card)
     t.con.Refresh()
   }
 }

Listing 4 nutzt für das obere Stockwerk in Abbildung 5 ein klickbares Canvas-Objekt. Normalerweise nimmt so ein nackter Fyne-Container keine Mausklicks entgegen. Deshalb legt Zeile 15 einen neuen Typ »tapOverlay« an, der durch Erweiterung des klickbaren Typs »BaseWidget« entsteht. Solche Custom-Widgets müssen mittels eines Renderers festlegen, wie sie auf dem Bildschirm erscheinen wollen. Zeile 27 definiert dazu die Funktion »CreateRenderer()«, die sich lediglich auf den Standard-Renderer für blanke Container beruft.

Später dargestellte Thumbnail-Fotos von Suchtreffern schaufelt »newCard()« ab Zeile 33 in einen Container, zusammen mit einem Label-Widget, das die Anzahl der Treffer zur angewählten Geokoordinate als Text ausgibt. Zunächst liest »Open« aus dem Paket »imaging« auf Github das anzuzeigende JPEG-Foto ein und korrigiert dessen Rotation, da Mobiltelefone Bilder aus Performance-Gründen oft rotiert abspeichern.

Die Funktion »Thumbnail()« aus »imaging« verkleinert das Foto dann geschwind auf die Größe 200 x 200. Das daraufhin in Fyne importierte Foto bekommt als Untermieter noch ein Label-Widget mit weißem Hintergrund spendiert. Schließlich stellt »NewVBox()« in Zeile 45 beide Widgets übereinander dar – wie eine Karteikarte, deswegen der Name »newCard()«.

Soll diese Karteikarte im Overlay erscheinen, ruft der Client »ShowThumb()« ab Zeile 47 auf und gibt ihr den Pfad zum Foto sowie die Klickkoordinaten als Parameter »pos« mit. Die neu erzeugte Karte bugsiert »Move()« in Zeile 50 an die richtige Stelle und »Add()« in Zeile 52 nimmt sie als Kind im Container auf. Soll die Karte hingegen wieder verschwinden, entzieht »Clear()« ab Zeile 56 dem Container das Kartenkind wieder.

Heimatliche Geodaten

Das Hauptprogramm in Listing 5 kombiniert alle bislang erörterten Funktionen in einer grafischen Desktop-Applikation. Den zentralen Geopunkt der dargestellten Karte definieren die Konstanten »CenterLat« und »CenterLon« ab Zeile 16. Sie stammen von Google Maps wie in Abbildung 6 für meinen Wohnort gezeigt.

Das Overlay-Widget aus Listing 4 bekommt ab Zeile 36 in Listing 5 noch einen Callback zur Seite gestellt, den das Widget aufruft, sobald ein Mausklick niedergeht. Der Callback-Code rechnet die X/Y-Koordinaten des Klicks in einen Geopunkt um. Danach sucht »findPhotosNear()« in Zeile 41 nach Maßgaben aus Listing 2 die Fotos aus der Datenbank zusammen, die in einem Kilometer Umkreis entstanden sind. Der zu Anfang in Zeile 23 initialisierte Random-Generator pickt bei mehreren Treffern in Zeile 46 einen zufälligen heraus, dessen Fotodatei daraufhin mit »ShowThumb()« in Zeile 47 in die Anzeige gelangt und über der Landkarte schwebt.

Abbildung 6: Startkoordinaten wie hier Mike Schillis Wohnung in San Francisco liefert Google Maps.

Abbildung 6: Startkoordinaten wie hier Mike Schillis Wohnung in San Francisco liefert Google Maps.

Damit Karte und Overlay übereinander zu liegen kommen, packt sie »NewStack()« in Zeile 58 in einen Stapel-Container. Die zuletzt genannten Widgets finden dabei obenauf Platz. Fyne verlangt von dargestellten Widgets übrigens, dass sie bekanntgeben, in welcher Größe der Renderer sie wiedergeben soll. Dabei spielt es eine Rolle, ob sie in einem Container mit oder ohne eingebautem Layout-Mechanismus lagern. Im ersteren Fall definiert die Widget-Funktion »SetMinSize()«, wie klein ein Container mit Layout-Funktion das Widget minimal zeichnen darf, wenn er Platz für andere Widgets braucht.

Handelt es sich um einen Container ohne Layout-Funktion, zum Beispiel ein nacktes Canvas-Element, muss ein darin enthaltenes Widget hingegen mit »Resize()« bekanntgeben, wie groß der Renderer es zeichnen soll. Wer vergisst, die maßgebliche Funktion festzulegen, rauft sich später die Haare, wenn Fyne die Widgets in Größe Null zeichnet und sie damit unsichtbar bleiben. Zum Glück ist der Slack-Channel des Projekts fast immer mit hochkarätigen Helfern besetzt.

Listing 5

gs.go

 package main
 import (
   "fmt"
   "os"
   "fyne.io/fyne/v2"
   "fyne.io/fyne/v2/app"
   "fyne.io/fyne/v2/canvas"
   "fyne.io/fyne/v2/container"
   "fyne.io/fyne/v2/widget"
   "math/rand"
   "time"
 )
 const (
   MapWidth  = 800
   MapHeight = 800
   CenterLat = 37.74769
   CenterLon = -122.42840
   RadiusKm  = 1.0
   Zoom      = 13
   AppName   = "Fyne Map Clicker 1.0"
 )
 func main() {
   rand.Seed(time.Now().UnixNano())
   myApp := app.New()
   myWindow := myApp.NewWindow(AppName)
   smap := NewSmap()
   err := smap.Init()
   if err != nil {
     panic(err)
   }
   mapImg := canvas.NewImageFromImage(smap.Image)
   mapImg.FillMode = canvas.ImageFillContain
   mapImg.SetMinSize(fyne.NewSize(MapWidth, MapHeight))
   found := []string{}
   overlay := newTapOverlay()
   overlay.cb = func(pos fyne.Position) {
     latlon := smap.Trans(pos.X, pos.Y)
     lat := latlon.Lat.Degrees()
     lon := latlon.Lng.Degrees()
     var err error
     found, err = findPhotosNear(lat, lon, RadiusKm)
     if err != nil {
       panic(err)
     }
     if len(found) > 0 {
       randomItem := found[rand.Intn(len(found))]
       overlay.ShowThumb(randomItem, len(found), fyne.NewPos(pos.X, pos.Y))
     } else {
       overlay.Clear()
     }
   }
   button := widget.NewButton("Accept", func() {
     for _, path := range found {
       fmt.Println(path)
     }
     os.Exit(0)
   })
   upper := container.NewStack(mapImg, overlay)
   upper.Resize(fyne.NewSize(MapWidth, MapHeight))
   gui := container.NewVBox(upper, button)
   myWindow.SetContent(gui)
   myWindow.Resize(fyne.NewSize(MapWidth, MapHeight))
   myWindow.ShowAndRun()
 }

Verketteung

Um den Button mit der Aufschrift »”Accept”« am unteren Bildrand kümmert sich die Variable »button« ab Zeile 52. Das Widget definiert einen Callback, der auf Knopfdruck die in »found« zuvor gefundenen Fotopfade zeilenweise auf Stdout ausgibt. Um also die Pfade aller für einen Geopunkt und dessen Suchradius aufgespürten Fotos auszugeben und die Applikation zu beenden, klickt der User Accept, woraufhin eine angeschlossene Unix-Pipe die Ausgabe aufschnappt und sie weiter bearbeitet. Zum Begutachten der Auswahl bietet sich die im Artikel “Freie Auswahl” [1] gezeigte Utility »photogrep« an.

Der übliche Dreisprung aus Listing 6 setzt sämtliche besprochenen Source-Dateien zu einem Binary »gs« zusammen, das auch den Code für alle abhängigen Pakete enthält. Der Bau der Utility »index« aus Listing 1 verläuft analog.

Listing 6

build.sh

$ go mod init gs
$ go mod tidy
$ go build gs.go near.go smap.go overlay.go

Fazit

Das Ganze würde übrigens nicht nur bei Programmschluss funktionieren, sondern auch während des Laufs, falls jemand laufend Kontaktabzüge sehen möchte. Wer aus der C-Welt kommt, wundert sich vielleicht, warum ein Programm, das zeilenweise Daten ausgibt, sie ohne extra Anweisungen auch ungepuffert zeilenweise durch eine angeschlossene Linux-Pipe mit angekoppelter »photogrep«-Utility schickt. Zur Erinnerung: Die C-Standard-Library gibt Daten nach Stdout zeilenweise aus, falls es sich beim Ausgabekanal um ein Terminal handelt. Hängt aber hinter der Applikation eine Linux-Pipe mit einer weiteren Applikation, sammelt glibc die Ausgabe in einem 4 kByte großen Puffer und entleert ihn, sobald er voll ist.

Das führt bei Entwicklern oft zu Frust, denn schreibt die pumpende Applikation lediglich eine Zeile und trödelt dann herum, erscheint in der Pipe – nichts. Erst mit einem »flush()« leert glibc einen erst teilweise gefüllten Ausgabepuffer. Die Überlegung bei diesem etwas verwirrenden Design war, dass es sich häufig tatsächlich als effizienter erweist, Ausgaben in einer Pipe zu puffern, und dass die Ausgabe in ein Terminal (meistens) sinnvollerweise zeilenweise erfolgt. Ganz anders in Go! Go puffert von Haus aus nicht. Wenn Sie puffern möchten, legen Sie mit dem Paket »bufio« einfach einen Stringpuffer an und spülen ihn erst bei Bedarf durch. Darüber stolpern allerdings viele Anfänger, die sich wundern, warum ihr Programm Ausgabedaten erstaunlich ineffizient in eine bereitstehende Pipe pumpt. (uba)

Infos

  1. Michael Schilli, “Freie Auswahl”, Linux-Magazin 01/2025: https://www.linux-magazin.de/ausgaben/2025/01/snapshot/
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 8 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
Nach oben