Schatzkarte (Linux-Magazin, Juli 2025)

In einer unaufgeräumten Fotosammlung findet sich ein gesuchter Schnappschuss oft schneller auf einer Landkarte als in der Timeline. Die Photo-App des Handys zeigt hierzu auf der Karte Fotogrüppchen an (Abbildung 1), und auf einen Fingerpieks hin offenbart sie eine Liste aller im Umkreis geschossenen Fotos. Wer weiß, woher eine Aufnahme stammt, findet so in vielen Fällen schnell das gesuchte Motiv.

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

Aufstöbern und archivieren

Heute bauen wir ein Go-Programm, das ähnliches leistet, aber wo beginnen? Bei tausenden von Fotos in einer tiefen Verzeichnishierarchie wäre es frevelhaft, diese 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 liegen. In einem Speicher wie -- einer Datenbank vielleicht? 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 allen dafür notwendigen Code mit in eine Applikation verpacken.

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

Listing 1: index.go

    01 package main
    02 import (
    03   "database/sql"
    04   "fmt"
    05   "log"
    06   "os"
    07   "path/filepath"
    08   "strings"
    09   _ "github.com/mattn/go-sqlite3"
    10   "github.com/rwcarlsen/goexif/exif"
    11 )
    12 const dbFile = "photos.db"
    13 func main() {
    14   if len(os.Args) < 2 {
    15     log.Fatal("Usage: " + os.Args[0] + " /images")
    16   }
    17   rootDir := os.Args[1]
    18   db, err := sql.Open("sqlite3", dbFile)
    19   if err != nil {
    20     log.Fatal(err)
    21   }
    22   defer db.Close()
    23   createTable := `
    24     CREATE TABLE IF NOT EXISTS photos (
    25       path TEXT PRIMARY KEY,
    26       lat REAL,
    27       lon REAL
    28     );`
    29   if _, err := db.Exec(createTable); err != nil {
    30     log.Fatal("Can't create table:", err)
    31   }
    32   err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
    33     if err != nil || info.IsDir() {
    34       return nil
    35     }
    36     ext := strings.ToLower(filepath.Ext(path))
    37     if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
    38       return nil
    39     }
    40     return Save(db, path)
    41   })
    42   if err != nil {
    43     log.Fatal(err)
    44   }
    45 }
    46 func Save(db *sql.DB, path string) error {
    47   f, err := os.Open(path)
    48   if err != nil {
    49     return err
    50   }
    51   defer f.Close()
    52   x, err := exif.Decode(f)
    53   if err != nil {
    54     return err
    55   }
    56   lat, lon, err := x.LatLong()
    57   if err != nil {
    58     return err
    59   }
    60   _, err = db.Exec(`INSERT OR REPLACE INTO photos (path, lat, lon) VALUES (?, ?, ?)`, path, lat, lon)
    61   if err != nil {
    62     log.Printf("Can't insert %s: %v\n", path, err)
    63     return err
    64   }
    65   fmt.Printf("Inserted: %s -> (%f, %f)\n", path, lat, lon)
    66   return nil
    67 }

Das fertig gebaute Binary index erwartet als ersten Kommandozeilenparameter einen Verzeichnisbaum mit Fotos, durchwandert ihn mit Walk aus dem filepath-Paket 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, legt sie das SQL-Kommando ab Zeile 24 neu an, mit den Spalten für den Pfad zum Foto, und den Float-Werten für lat und lon. Auch der Insert-Befehl ab Zeile 60 kann mit bereits bestehenden Daten umgehen, dank INSERT OR REPLACE frischt der Engine die Werte bereits abgelegter Fotos allerhöchstens auf, aber trägt keine doppelten Zeilen ein. So lässt sich das Programm beliebig oft aufrufen, "Idempotenz" sagt der Fachmann dazu. Abbildung 2 zeigt die mit schlappen 3000 Fotos gefüllte SQLite-Datenbank nach Abschluss des Suchlaufs von index.

Abbildung 3: Die fertige Fyne-Applikation

Fotos nah und fern

Die fertige Applikation soll später wie in Abbildung 3 aussehen. Klickt der User auf einen Punkt der Landkarte, wird das Programm ausrechnen, auf welchen Geokoordinaten der gewählte Punkt liegt. Um herauszufinden, welche Fotos im Umkreis dieses Punktes geschossen wurden, definiert Listing 2 die Funktion findPhotosNear(), die alle in der Datenbank gespeicherten Fotos durchläuft und diejenigen meldet, deren geografischer Abstand zum Referenzpunkt kleiner als der Suchradius radiusKm ist.

Listing 2: near.go

    01 package main
    02 import (
    03   "database/sql"
    04   geo "github.com/kellydunn/golang-geo"
    05   _ "github.com/mattn/go-sqlite3"
    06 )
    07 type Photo struct {
    08   Path string
    09   Lat  float64
    10   Lon  float64
    11 }
    12 func findPhotosNear(lat, lon float64, radius float64) ([]string, error) {
    13   db, err := sql.Open("sqlite3", "photos.db")
    14   if err != nil {
    15     return nil, err
    16   }
    17   defer db.Close()
    18   rows, err := db.Query(`SELECT path, lat, lon FROM photos`)
    19   if err != nil {
    20     return nil, err
    21   }
    22   defer rows.Close()
    23   results := []string{}
    24   for rows.Next() {
    25     var p Photo
    26     if err := rows.Scan(&p.Path, &p.Lat, &p.Lon); err != nil {
    27       return nil, err
    28     }
    29     p1 := geo.NewPoint(lat, lon)
    30     p2 := geo.NewPoint(p.Lat, p.Lon)
    31     if p1.GreatCircleDistance(p2) <= radius {
    32       results = append(results, p.Path)
    33     }
    34   }
    35   return results, nil
    36 }

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 jeweiligen Fotos als Fließkommawerte. Ist die Distanz dieses Geopunktes vom gewählten Referenzstandort kleiner als der eingestellte Suchradius, schafft es das Foto in die Auswahl.

Wie bestimmt sich nun die Distanz zweier Geopunkte auf der Erdkugel? Das könnte man aufgrund der geringen Entfernung vereinfacht 2D-geometrisch lösen, aber das Paket golang-geo auf Github bietet die praktische Funktion GreatCircleDistance(), die die gesuchte Distanz zweier Punkte auf der 3D-Oberfläche der Erde bequem als Fließkommawert in Kilometern liefert. Fertig ist der Selektor von Bildern in einem festen Umkreis eines dynamisch gewählten Ortes, nun geht es an die grafische Darstellung in einer Desktop-Applikation.

Karte, kannste klicken

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

Die GUI der Applikation wird später wie in Abbildung 3 eine Landkarte anzeigen, die Mausklicks oder Finger-Taps entgegennimmt. Freies Kartenmaterial bietet das Openstreetmap-Projekt, Abbildung 4 zeigt meine Wahlheimat San Francisco auf der OSM-Webseite im Browser. Das Projekt erlaubt aber nicht nur den Zugriff auf die Web-Applikation, sondern liefert das Kartenmaterial auch an selbstgeschriebene Desktop-Applikationen. Karten in verschiedenen Auflösungen (oder Zoom-Settings nach Openstreetmap-Jargon) liefern die sogenannten Tile-Server des Projekts als einzelne quadratische Ausschnitte, sogenannte Kacheln, die der Client anschließend nahtlos aneinanderlegt, um die große Karte auf den Schirm zu bringen.

Listing 3: smap.go

    01 package main
    02 import (
    03   "github.com/flopp/go-staticmaps"
    04   "github.com/golang/geo/s2"
    05   "image"
    06   "os"
    07 )
    08 type Smap struct {
    09   Image image.Image
    10   Trans func(x, y float32) s2.LatLng
    11 }
    12 func NewSmap() *Smap {
    13   return &Smap{}
    14 }
    15 func (smap *Smap) Init() error {
    16   ctx := sm.NewContext()
    17   ctx.SetSize(MapWidth, MapHeight)
    18   ctx.SetUserAgent(AppName)
    19   cache := sm.NewTileCache("cache", os.ModePerm)
    20   ctx.SetCache(cache)
    21   center := s2.LatLngFromDegrees(CenterLat, CenterLon)
    22   ctx.SetCenter(center)
    23   ctx.SetZoom(Zoom)
    24   var err error
    25   smap.Image, err = ctx.Render()
    26   if err != nil {
    27     return err
    28   }
    29   trans, err := ctx.Transformer()
    30   if err != nil {
    31     return err
    32   }
    33   trueCenter := s2.LatLngFromDegrees(CenterLat, CenterLon)
    34   cx, cy := trans.LatLngToXY(trueCenter)
    35   smap.Trans = func(x, y float32) s2.LatLng {
    36     fx := cx - float64(MapWidth)/2 + float64(x)
    37     fy := cy - float64(MapHeight)/2 + float64(y)
    38     return trans.XYToLatLng(fx, fy)
    39   }
    40   return nil
    41 }

Dank des Pakets go-staticmaps (ein Klon des gleichnamigen Python-Projekts) auf Github gestaltet sich das Einholen der Tiles für eine vorgegebene Geo-Location recht zügig, wie Listing 3 zeigt. Weiter liegt dem Paket eine Transformer-Funktion bei, die Klicks als X/Y Koordinaten in Pixeln auf der lokal dargestellten Karte wieder in Längen- und Breitengrade auf der Erde zurückrechnet.

Leider funktioniert der in go-staticmaps eingebaute Transformator nicht ganz so, wie man es vielleicht erwarten würde. Gibt eine Applikation eine Geokoordinate an, liegt diese nur in den seltensten Fällen genau in der Mitte einer dargestellten Kachel. In diesen Fällen sind die vom Transformator errechneten Längen- und Breitengrade aber verschoben, um den Offset innerhalb der Kachel. Das scheint ein Bug im Paket zu sein, als Workaround definiert Listing 3 im Typ Smap 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. Puh!

Wohl erzogen

Zwar stellt das Open-Street-Map-Projekt die Kartendaten kostenlos zur Verfügung, laut Webseite kann es aber nicht unbegrenzt Serverkapazitäten finanzieren. Darum begrenzt der Service den Zugriff, und zwar per User-Agent. Wer nun eine Standard-Go-Library with net/http einsetzt, wird sich wundern, dass der darin intern verwendete User-Agent bereits geblockt wird. Listing 5 setzt ihn deshalb deshalb später explizit auf "Fyne Map Clicker 1.0", und den behandelt Openstreetmap (noch) freundlich. Jedenfalls bis Millionen von Lesern des Linux-Magazins den Code kopieren und auf Openstreetmap einhämmern!

Ohne Zutun holt sich das Paket go-staticmaps die Kachel des dargestellten Landkartenausschnitts bei jedem Programmstart erneut vom Server. Das ist natürlich Kappes, denn das beansprucht nicht nur unnötigerweise Server-Resourcen 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. Letzteres legt das Paket selbständig an, falls es noch nicht existiert und speichert darunter nicht nur die aktuell benötigeten Kacheln, sondern auch noch die angrenzenden, was Applikationen, die dem User das herumschubsen der Landkarte erlauben, zu schätzen wissen.

Lagen-Look

Abbildung 5: Unten der Openstreetmap-Container, obenauf ein klickbares Foto als Canvas-Objekt

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

Listing 4: overlay.go

    01 package main
    02 import (
    03   "fyne.io/fyne/v2"
    04   "fyne.io/fyne/v2/canvas"
    05   "fyne.io/fyne/v2/container"
    06   "fyne.io/fyne/v2/widget"
    07   "github.com/disintegration/imaging"
    08   "golang.org/x/text/language"
    09   "golang.org/x/text/message"
    10   "image/color"
    11 )
    12 const (
    13   ThumbSize = 200
    14 )
    15 type tapOverlay struct {
    16   widget.BaseWidget
    17   con  *fyne.Container
    18   card *fyne.Container
    19   cb   func(fyne.Position)
    20 }
    21 func newTapOverlay() *tapOverlay {
    22   over := &tapOverlay{}
    23   over.ExtendBaseWidget(over)
    24   over.con = container.NewWithoutLayout()
    25   return over
    26 }
    27 func (t *tapOverlay) CreateRenderer() fyne.WidgetRenderer {
    28   return widget.NewSimpleRenderer(t.con)
    29 }
    30 func (t *tapOverlay) Tapped(pe *fyne.PointEvent) {
    31   t.cb(pe.Position)
    32 }
    33 func (t *tapOverlay) newCard(fileName string, total int) *fyne.Container {
    34   img, err := imaging.Open(fileName, imaging.AutoOrientation(true))
    35   if err != nil {
    36     panic(err)
    37   }
    38   thumbnail := imaging.Thumbnail(img, int(ThumbSize), int(ThumbSize), imaging.Lanczos)
    39   image := canvas.NewImageFromImage(thumbnail)
    40   image.FillMode = canvas.ImageFillContain
    41   image.SetMinSize(fyne.NewSize(ThumbSize, ThumbSize))
    42   p := message.NewPrinter(language.English)
    43   label := widget.NewLabel(p.Sprintf("%d", total))
    44   bg := canvas.NewRectangle(color.White)
    45   return container.NewVBox(image, container.NewMax(bg, label))
    46 }
    47 func (t *tapOverlay) ShowThumb(path string, total int, pos fyne.Position) {
    48   t.Clear()
    49   card := t.newCard(path, total)
    50   card.Move(pos)
    51   card.Resize(fyne.NewSize(ThumbSize, ThumbSize))
    52   t.con.Add(card)
    53   t.card = card
    54   canvas.Refresh(t)
    55 }
    56 func (t *tapOverlay) Clear() {
    57   if t.card != nil {
    58     t.con.Remove(t.card)
    59     t.con.Refresh()
    60   }
    61 }

Listing 4 nutzt für das obere Stockwerk in Abbildung 5 ein klickbares Canvas-Objekt. Normalerweise nimmt so 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 hierzu die Funktion CreateRenderer(), die sich lediglich auf den Standard-Renderer für nackte Container beruft.

Später dargestellte Thumbnail-Fotos von Suchtreffern wickelt newCard() ab Zeile 33 in einen Container, zusammen mit einem Label-Widget, das die Anzahl der Treffer zur angewählten Geokoordinate als Text anzeigt. Erst liest Open aus dem Paket imaging auf Github das anzuzeigende Jpg-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 200x200. Das daraufhin in Fyne importierte Foto bekommt als Untermieter noch ein Label-Widget mit weißem Hintergrund spendiert, und NewVBox() in Zeile 45 stellt beide Widgets übereinander dar, ganz 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. Lustig ist das Containerleben.

Heimatliche Geodaten

Das Hauptprogramm in Listing 5 kombiniert nun alle bislang erörterten Funktionen in eine grafische Desktop-Applikation. Den zentralen Geopunkt der dargestellten Karte definieren die Konstanten CenterLat und CenterLon ab Zeile 16. Sie wurden über Google Maps wie in Abbildung 6 gezeigt für meinen Wohnort ermittelt. Das Overlay-Widget aus Listing 4 bekommt ab Zeile 36 noch einen Callback spendiert, den das Widget immer dann aufruft, falls ein Mausklick niedergeht. Der Callback-Code rechnet die X/Y-Koordinaten des Klicks dann in einen Geopunkt um und findPhotosNear() in Zeile 41 sucht nach Listing 2 die Fotos aus der Datenbank zusammen, die in einem Kilometer Umkreis geschossen wurden. Der zu Anfang in Zeile 23 initialisierte Random-Generator pickt bei mehreren Treffern in Zeile 46 einen zufälligen heraus, dessen Fotodatei dann 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

Damit Karte und Overlay übereinander zu liegen kommen, packt sie NewStack() in Zeile 58 in einen Stapel-Container. Die zuletzt genannten Widgets kommen dabei obenauf zu liegen. Fyne verlangt von dargestellten Widgets übrigens, dass sie bekanntgeben, in welcher Größe der Renderer sie darstellen soll. Dabei spielt es eine Rolle, ob sie in einem Container mit oder ohne eingebautem Layout-Mechanismus zu liegen kommen. 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. Im Falle eines Containers 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, sie also unsichtbar bleiben. Zum Glück ist der Slack-Channel des Projekts fast immer mit hochkarätigen Helfern besetzt.

Listing 5: gs.go

    01 package main
    02 import (
    03   "fmt"
    04   "os"
    05   "fyne.io/fyne/v2"
    06   "fyne.io/fyne/v2/app"
    07   "fyne.io/fyne/v2/canvas"
    08   "fyne.io/fyne/v2/container"
    09   "fyne.io/fyne/v2/widget"
    10   "math/rand"
    11   "time"
    12 )
    13 const (
    14   MapWidth  = 800
    15   MapHeight = 800
    16   CenterLat = 37.74769
    17   CenterLon = -122.42840
    18   RadiusKm  = 1.0
    19   Zoom      = 13
    20   AppName   = "Fyne Map Clicker 1.0"
    21 )
    22 func main() {
    23   rand.Seed(time.Now().UnixNano())
    24   myApp := app.New()
    25   myWindow := myApp.NewWindow(AppName)
    26   smap := NewSmap()
    27   err := smap.Init()
    28   if err != nil {
    29     panic(err)
    30   }
    31   mapImg := canvas.NewImageFromImage(smap.Image)
    32   mapImg.FillMode = canvas.ImageFillContain
    33   mapImg.SetMinSize(fyne.NewSize(MapWidth, MapHeight))
    34   found := []string{}
    35   overlay := newTapOverlay()
    36   overlay.cb = func(pos fyne.Position) {
    37     latlon := smap.Trans(pos.X, pos.Y)
    38     lat := latlon.Lat.Degrees()
    39     lon := latlon.Lng.Degrees()
    40     var err error
    41     found, err = findPhotosNear(lat, lon, RadiusKm)
    42     if err != nil {
    43       panic(err)
    44     }
    45     if len(found) > 0 {
    46       randomItem := found[rand.Intn(len(found))]
    47       overlay.ShowThumb(randomItem, len(found), fyne.NewPos(pos.X, pos.Y))
    48     } else {
    49       overlay.Clear()
    50     }
    51   }
    52   button := widget.NewButton("Accept", func() {
    53     for _, path := range found {
    54       fmt.Println(path)
    55     }
    56     os.Exit(0)
    57   })
    58   upper := container.NewStack(mapImg, overlay)
    59   upper.Resize(fyne.NewSize(MapWidth, MapHeight))
    60   gui := container.NewVBox(upper, button)
    61   myWindow.SetContent(gui)
    62   myWindow.Resize(fyne.NewSize(MapWidth, MapHeight))
    63   myWindow.ShowAndRun()
    64 }

Verkettetung von Umständen

Für den Button mit der Aufschrift "Accept" am unteren Bildrand zeichnet die Variable button ab Zeile 52 verantwortlich. Das Widget definiert einen Callback, der auf Knopfdruck die in found vorher gefundenen Fotopfade zeilenweise auf Stdout ausgibt. Um also die Pfade aller für einen Geopunkt und dessen Suchradius gefundenen Fotos auszugeben und die Applikation zu beenden, klickt der User "Accept" und eine angeschlossene Unix-Pipe schnappt die Ausgabe auf und bearbeitet sie weiter. Zum Begutachten der Auswahl bietet sich die in [2] gezeigte Utility photogrep an.

Der übliche Dreisprung aus Listing 6 baut alle 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

    1 $ go mod init gs
    2 $ go mod tidy
    3 $ go build gs.go near.go smap.go overlay.go

Tröpfchenweise oder volles Rohr

Dies würde übrigens nicht nur bei Programmschluss funktionieren, sondern auch während des Laufs, falls jemand laufend Kontaktabzüge sehen wollte. Wer aus der C-Welt kommt, wundert sich vielleicht, warum ein Programm, das zeilenweise Daten ausgibt, diese ohne extra Anweisungen auch ungepuffert zeilenweise durch eine angeschlossene Linux-Pipe mit angekoppelter photogrep-Utility schickt. Wir erinnern uns: 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 weitere Applikation, sammelt glibc die Ausgabe in einem 4kB großen Puffer, und entleert diesen, wenn er voll ist. Das führt bei Entwicklern oft zu Haareraufen, denn schreibt die pumpende Applikation nur eine Zeile und trödelt dann herum, erscheint in der Pipe -- nichts. Erst mit einem flush() leert glibc einen erst teilweise gefüllter Ausgabepuffer. Die Überlegung bei diesem etwas verwirrende Design war, dass es meist tatsächlich effizienter ist, Ausgaben in einem Pipe zu puffern, und dass die Ausgabe in ein Terminal (ebenfalls meistens) sinnvollerweise zeilenweise erfolgt. Ganz anders in Go! Go puffert von Haus aus nicht. Wer puffern möchte, kann mit dem Paket bufio einfach einen Stringpuffer anlegen und diesen erst bei Bedarf durchspülen. Darüber stolpern allerdings viele Anfänger, die sich wundern, warum ihr Programm Ausgabedaten erstaunlich ineffizient in eine bereitstehende Pipe pumpt.

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2025/07/snapshot/

[2]

Michael Schilli, "Freie Auswahl", Snapshot-Artikel zur Utility "photogrep", https://www.linux-magazin.de/ausgaben/2025/01/snapshot/

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 22:

Unknown directive: =desc

Around line 703:

Unterminated C<...> sequence