Go Interfaces: image.Image und draw.Image implementieren

Go bietet eine Vielzahl von praktischen Interfaces. Wie du diese implementierst und welche Vorteile das bringt, erkläre ich in diesem Artikel.

Für mich war Go die erste Sprache, welche das Konzept der impliziten Interfaces mitbringt. Um die Implementierung von Interfaces in Go zu verstehen, sollte man auch dieses etwas ungewöhnliche Konzept kennen. Wenn dir das alles nicht neu ist, kannst du auch direkt zum Use Case weiter scrollen.

Aus der OOP von beispielsweise Java oder PHP kennen wir die explizite Angabe einer Schnittstelle, welche von einer Klasse implementiet werden soll:

<?php
class MyClass implements MyInterface {
    // ...
}
?>

MyClass muss jetzt die von MyInterface benötigten Methoden aufweisen, sonst wirft einem der PHP-Interpreter einen Fatal Error um die Ohren. Bei Go sind Interfaces hingegen implizit und müssen nicht angegeben werden. In einfachen Worten bedeutet das, dass alle struct’s, welche “zufällig” die korrekten Funktionen aufweisen, automatisch das entsprechende Interface implementieren.
Nehmen wir folgendes Beispiel:

package main

type MyInterface interface {
    IsEmpty (string) bool
}

type MyStruct struct {
    // ...
}

Wir haben ein Interface und eine struct definiert. Noch kennen sich diese Typen nicht, aber das ändern wir indem wir die Funktion IsEmpty() hinzufügen:

func (my MyStruct) IsEmpty(a string) bool {
    return a == ""
}

MyStruct implementiert nun MyInterface und bringt altbekannte Vorteile mit sich. Zum Beispiel können von Funktionen erwartete Parameter vom Interface-Typ sein und so die Möglichkeiten der Dependency Injection enorm erweitern.

Use Case: Meine Identicon Bibliothek

Vor einiger Zeit habe ich die Go-Bibliothek identicon veröffentlicht. Mit ihrer Hilfe kann man - wer hätte es erraten - sogenannte Identicons erzeugen. Ein eindeutiger Identifikationsstring führt dabei immer zu den selben Bilddaten.

Pre-Interface

Zu einem früheren Zeitpunkt war die Bibliothek so geschrieben, dass man mit Hilfe einer mit identicon.New() erstellten Instanz und deren Methode GenerateImage() ein konkretes *image.NRGBA bekommen hat, welches man dann verwenden konnte:

package main

// imports ...

func main() {
    fi, _ := os.Create("identicon.png")
    defer fi.Close()

    // Instanz erstellen
    ic, err := identicon.New("oh-hello@my-identicon.com", nil)
    if err != nil {
        panic(err.Error())
    }

    // GenerateImage() aufrufen, um erzeugtes Bild zu bekommen
    png.Encode(fi, ic.GenerateImage())
}

Die dazugehörigen Funktionsköpfe aus der Bibliothek:

package identicon

// New returns a new identicon based on given ID string
func New(ID string, opts *Options) (*Identicon, error) { }

// GenerateImage returns an generated Image representation of the identicon
func (ic *Identicon) GenerateImage() *image.RGBA { }

So weit, so gut. Für mich war die Sache weitestgehen erledigt, schließlich kann man das *image.NRGBA wunderbar als Bilddatei ablegen. Doch dann schrieb mir der User /u/hexaga auf reddit folgendes:

“Nice! I would recommend trying to implement image.Image for your Identicon struct directly, it’s not a complicated interface and would prevent some superfluous drawing at runtime. As a bonus, resizing the image becomes almost free.”

Der Vorschlag klang absolut einleuchtend. Mich hatten von Anfang an zwei Tatsachen an der Bibliothek gestört:

  • der Aufruf von GenerateImage() - wenn auch nicht kompliziert - schien mir immer unnötig, mein Identicon-struct alleine sollte doch ausreichen
  • das konkrete *image.NRGBA ist in seiner Verwendung eingeschränkt

Ich habe mir das image.Image-Interface und dessen Verwendung in der Standard-Bibliothek etwas genauer angeschaut. Schließlich stieß ich auch auf das draw.Image-Interface, welches ähnlich hilfreich erscheint (dazu später mehr) und selbst image.Image implementiert.

image.Image implementieren

Diese Schnittstelle setzt drei Funktionen voraus:

  1. ColorModel() color.Model

    Die Rückgabe dieser Funktion ist selbst ein Interface und definiert sich folgendermaßen:

    “Model can convert any Color to one from its own color model. The conversion may be lossy.”

    Praktischerweise gibt es das color-Packet. Es enthält ein paar Standardfarbräume, bei denen wir uns bedienen können.

  2. Bounds() Rectangle

    Auf Anfrage antwortet diese Funktion mit einem Rechteck, den Ausmaßen unseres Bildes.

  3. At(x, y int) color.Color

    Diese Funktion liefert den Farbwert des Bildes an einer x/y-Position mit einem color.Color-Typ, ebenfalls ein Interface. Die Implementierung der At()-Funktion habe ich mir in der Standardbibliothek am Typ image.NRGBA abgeschaut.

Der vollständige Code fällt relativ gering aus:

package identicon

func (ic *Identicon) ColorModel() color.Model {
    return color.NRGBAModel
}

func (ic *Identicon) Bounds() image.Rectangle {
    // Rect wird von identicon.New() befüllt
    return ic.Rect
}

func (ic *Identicon) At(x, y int) color.Color {
    return ic.NRGBAAt(x, y)
}

Sind diese Funktionen da, erhalten wir die neue Möglichkeit, unser Identicon struct direkt für viele Funktionen der stdlib nutzen zu können. Beispielsweise beim speichern des Bildes in eine Datei (png.Encode()).

draw.Image implementieren

Das draw-Paket bietet zwei Funktionen, mit deren Hilfe man simpel rechteckige Flächen auf einem Bild mit einer Farbe füllen kann. Das möchte ich zum Beispiel machen, wenn ich den Hintergrund meines Bild malen möchte. Damit das funktionieren kann, müssen wir draw.Image mit lediglich einer Funktion implementieren:

  1. Set(x, y int, c color.Color)
    Speichert einen Farbwert an x/y-Koordinaten. Auch diese Funktion konnte ich mir bei image.NRGBA abschauen.

Da der draw.Image-Typ außerdem per compositing den image.Image-Typen einbettet, benötigen wir auch dessen Funktionen (☑️ erledigt).

package identicon

func (ic *Identicon) Set(x, y int, c color.Color) {
    if !(image.Point{x, y}.In(ic.Rect)) {
        return
    }

    i := ic.PixOffset(x, y)
    c1 := ic.ColorModel().Convert(c).(color.NRGBA)
    ic.Pix[i+0] = c1.R
    ic.Pix[i+1] = c1.G
    ic.Pix[i+2] = c1.B
    ic.Pix[i+3] = c1.A
}

Wir mussten gerade mal eine handvoll relativ kurzer Funktionen schreiben, um zwei weit verbreitete Interfaces der Standardbibliothek zu implementieren. Tatsächlich gibt es jede Menge solcher Schnittstellen, die unseren Code sehr flexibler und besser einsetzbar machen.
In einem folgenden Artikel werde ich noch auf meine Favoriten eingehen und erzählen, wie sie mir geholfen haben.

Auch interessant