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, meinIdenticon
-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:
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.Bounds() Rectangle
Auf Anfrage antwortet diese Funktion mit einem Rechteck, den Ausmaßen unseres Bildes.
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 derAt()
-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:
Set(x, y int, c color.Color)
Speichert einen Farbwert an x/y-Koordinaten. Auch diese Funktion konnte ich mir beiimage.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.