Go macht es einfach, universellen Code in ein Paket zu packen und auf Github mit der Welt zu teilen. Mike Schilli erläutert die notwendigen Kniffe und umgeht Stolperfallen.
Aufmerksamen Lesern dieser Kolumne ist sicher schon aufgefallen, dass die vorgestellten Go-Listings oft Pakete auf Github referenzieren, die der Go-Compiler klaglos dort abholt und als Library in geschnürte Binaries einbindet. Aber immer nur nehmen ist nicht schön! Wie schwer wäre es wohl, selbst Code zu schreiben und ihn auf Github mit der Welt zu teilen, auf dass Programmierfüchse nah und fern ihn nutzen, Zeit sparen und Lobeshymnen auf den Autor singen könnten?
Nützlich wäre zum Beispiel ein simples Paket, das es einer Go-Applikation erlaubt, von ihr verwendete Passwörter und API-Tokens in eine externe Datei auszulagern. Diese Strings sollten keinesfalls direkt im Code stehen, und nicht nur deshalb, weil die Listings hier abgedruckt im Heft stehen. Im Produktionsbetrieb sind hartkodierte Strings ebenfalls nicht beliebt, weil der Code meist offen in einem Github-Repo liegt und automatische Installationen gern Binaries und Geheimnisse getrennt ausrollen – ganz so, als würde der User sie nach der Installation von Hand konfigurieren.
Nehmen wir die Beispielapplikation aus Listing 1, die die fünf am häufigsten abgerufenen Videos eines Youtube-Channels ermittelt und dafür einen geheimen API-Key und eine Channel-ID braucht. Statt Strings mit geheimen Daten im Code zu halten, ruft der Code zweimal die Funktion »Lookup()« auf. Sie liest aus einer externen menschenlesbaren Datei einen String zu dem angegebenen Stichwort (im Beispiel einmal “youtube-api-key” und einmal “youtube-channel-id”), sucht danach und reicht das Ergebnis an die Applikation zurück.
Listing 1
yttop.go
package main
import (
"context"
"fmt"
"github.com/mschilli/go-murmur"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
"log"
)
func main() {
m := murmur.NewMurmur()
apiKey, err := m.Lookup("youtube-api-key")
if err != nil {
panic(err)
}
channelID, err := m.Lookup("youtube-channel-id")
if err != nil {
panic(err)
}
ctx := context.Background()
service, err := youtube.NewService(ctx, option.WithAPIKey(apiKey))
videoIDs := make([]string, 0)
call := service.Search.List([]string{"id"}).ChannelId(channelID).MaxResults(5).Order("viewCount")
resp, err := call.Do()
if err != nil {
log.Fatalf("Error making search API call: %v", err)
}
for _, item := range resp.Items {
if item.Id.Kind == "youtube#video" {
videoIDs = append(videoIDs, item.Id.VideoId)
}
}
videoCall := service.Videos.List([]string{"snippet", "statistics"}).Id(videoIDs...)
videoResponse, err := videoCall.Do()
if err != nil {
log.Fatalf("Error making videos API call: %v", err)
}
for _, item := range videoResponse.Items {
fmt.Printf("%6d %.40s (%s)\n", item.Statistics.ViewCount, item.Snippet.Title, item.Id)
}
}
Leise murmeln
Die praktische Utility-Funktion »Lookup()« ist Teil eines neu erzeugten Go-Pakets, das alle Programmierer dieser Welt aus meinem öffentlichen Github-Account direkt in ihren Go-Code einbinden können. Da man geheime Dinge nicht lauthals ausposaunen sollte, sondern allenfalls murmeln oder raunen, soll das Package Murmur (Englisch für murmeln) heißen.
Dabei kommt das Paket objektorientiert daher. Die Applikation aus Listing 1 ruft in Zeile 11 den Konstruktor »murmur.NewMurmur()« auf. Zeile 5 zieht das Paket vorher per URL unter dem Pfad »mschilli/go-murmur« von Github herein. Das Vorgehen folgt der Konvention, dass Github-Repos, die Go-Pakete enthalten, immer mit »go-*« beginnen. Intern definiert das Paket seinen Namen allerdings als »murmur«, nicht als »go-murmur«. Wie das funktioniert, zeigt seine Implementierung weiter unten in Listing 2.
Damit die Methode »Lookup()« des erzeugten Objekts »m« das Geheimnis zu einem Schlüssel wie »youtube-api-key« findet, muss sie die YAML-Datei aus Abbildung 1 aufspüren, in der die Schlüssel auf Geheimnisse zeigen. Normalerweise liegt die Datei namens ».murmur« im Home-Verzeichnis des Users. Der darf aber dem Konstruktor einen alternativen Suchpfad mitgeben, wie wir später sehen.
Kanal-Hitparade
Der Rest von Listing 1 zum Einholen der fünf populärsten Videos aus meinem Youtube-Channel ist ein typischer Fall für die Youtube-API. Deren Version 3 zieht Zeile 7 herein, dieses Mal nicht von Github, sondern von den Google-Leuten auf »golang.org«. Zeile 21 erzeugt ein neues Service-Objekt zur Kommunikation mit dem Youtube-API-Server und übergibt einen vorher von der Youtube Developer Console abgeholten und in der Murmur-Datei abgelegten API-Key. So weiß der Server auch, mit wem er es zu tun hat – Fremden gegenüber zeigt er sich nämlich verschlossen. Was zu tun ist, um einen API-Key für Entwickler sowie die Channel-ID eines Youtube-Kanals zu erhalten, war Thema im Snapshot der Ausgabe 01/2024 [1].
Die Funktion »Search.List()« in Zeile 23 listet zu einem vorgegebenen Youtube-Channel anhand seiner Channel-ID die als »MaxResults« angegebenen fünf Videos, die sie nach »”viewCount”« ordnet, also nach ihrer Popularität. Zurück kommen normalerweise fünf Treffer, über die die For-Schleife ab Zeile 28 iteriert. Sie steckt die IDs gefundener Videos in den Array-Slice »videoIDs«.
Da die Trefferliste zwar zu jedem Clip einige Daten liefert, jedoch noch keine Klickzahlen, gibt Zeile 33 vorher gefundene IDs an einen weiteren Aufruf der List-Funktion weiter. Die holt mit »”statistics”« die gesammelten Besucherstatistiken zu diesen Videos ein. Derart gewappnet, gibt die For-Schleife ab Zeile 38 die Top-5-Liste der erfolgreichsten Videos des Channels samt Besucherzahlen aus (Abbildung 2).
Doch in dieser Ausgabe soll sich der Fokus nicht auf Youtube richten, sondern auf dem selbst geschnürten Paket murmur, das mittlerweile auf Github liegt. Der Standard-Dreisprung zum Bauen des Hitparaden-Binarys aus Listing 1 zieht es heran, ganz wie vorher die Youtube-API. Abbildung 3 zeigt, wie »go mod tidy« das Paket auf Github in der Version 1.0.0 findet und abholt. Der Go-Compiler linkt alles zusammen, und das fertig kompilierte Binary »yttop« präsentiert wie gewünscht die Liste der erfolgreichsten Videos des Channels.
Selbst gebaut
Wie sieht nun die selbst gebaute Paket-Library murmur aus? Und wie landet sie auf Github, damit Entwickler, die »go mod tidy« aufrufen, sie finden und in ihren Code einbinden können? Der Konstruktor »NewMurmur()« ab Zeile 16 in Listing 2 erzeugt eine Struktur vom Typ »MurmurStore« und gibt an den Aufrufer einen Pointer darauf zurück. Der dient ihm fürderhin als Objekt für folgende Methodenaufrufe.
Listing 2
murmur.go
package murmur
import (
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"os/user"
"path"
)
const Version = "1.0.1"
// Read secrets from a .murmur YAML file
type Murmur struct {
FilePath string
}
const StoreFileName = ".murmur"
// Create a new instance
func NewMurmur() *Murmur {
return &Murmur{}
}
// Set the .murmur file path manually
func (m *Murmur) WithFilePath(path string) *Murmur {
m.FilePath = path
return m
}
func homePath() (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
p := path.Join(u.HomeDir, StoreFileName)
return p, nil
}
// Look up a .murmur key by name and return its value
func (m *Murmur) Lookup(name string) (string, error) {
if len(m.FilePath) == 0 {
path, err := homePath()
if err != nil {
return "", err
}
m.FilePath = path
}
dict, err := readYAMLFile(m.FilePath)
if err != nil {
return "", err
}
pass, ok := dict[name]
if !ok {
return "", fmt.Errorf("No entry found for %s", name)
}
return pass, nil
}
func readYAMLFile(path string) (map[string]string, error) {
data := make(map[string]string)
raw, err := ioutil.ReadFile(path)
if err != nil {
return data, err
}
err = yaml.Unmarshal(raw, &data)
if err != nil {
return data, err
}
return data, nil
}
Einem Konstruktor Parameter mitzugeben (zum Beispiel den Pfad der ».murmur«-Datei), ist in Go nicht standardisiert und wegen strenger Typisierung nicht sauber durch variable Parameterlisten lösbar. Das selbst gestrickte Paket murmur entscheidet sich dafür, den Konstruktor ohne Parameter zu definieren und eine später optional aufgerufene Funktion »WithFilePath()« (ab Zeile 19) auf das Objekt anzubieten. Die setzt den Pfad zur Geheimnisdatei als String in der Objektstruktur. Der Modifizierer gibt selbst wieder einen Pointer auf die Objektstruktur zurück, sodass sich später mehrere Modifizierer verketten lassen.
Stellt die Methode »Lookup()« ab Zeile 31 fest, dass der User einen Wert holen möchte, aber bislang noch kein Pfad vorliegt, sucht sie im Home-Verzeichnis nach einer Datei namens ».murmur« und meldet einen Fehler, falls sie dort nichts findet. Das erfolgt nur beim ersten Aufruf, ab dann ist der Pfad in der Objektstruktur gesetzt.
Die JSON-Daten liest »readYAMLFile()« (klein geschrieben, weil nicht exportiert) ab Zeile 51 aus der Datei in eine Datenstruktur vom Typ »map«. Zeile 45 prüft, ob der angegebene Schlüssel definiert ist. Liegt er vor, gibt Zeile 49 den zugehörigen Wert zurück, also das Geheimnis, während bei einer erfolglosen Suche Zeile 47 dem Aufrufer einen Fehler meldet.
Listing 3
murmur_test.go
package murmur
import (
"testing"
)
func TestLookup(t *testing.T) {
mur := NewMurmur().WithFilePath("data/murmur.yaml")
name := "foo"
p, err := mur.Lookup(name)
if err != nil {
t.Log("name", name, "not found")
t.Fail()
}
if p != "bar" {
t.Log("name", name, "p", p, "mismatch")
t.Fail()
}
name = "nonexist"
p, err = mur.Lookup(name)
if err == nil {
t.Log("name", name, "found")
t.Fail()
}
}
Beruhigende Tests
So weit das hoffentlich praktische neue Paket; der Code ist schön kompakt geblieben. Aber auch geübte Programmierer sehen einem Stück Code selten an, ob es tatsächlich funktioniert. Deshalb sollte ein auf Github abgestelltes Go-Paket immer Tests enthalten, die sich mit »go test« auf der Kommandozeile ausführen lassen und entweder Erfolg oder einen Fehler melden.
Listing 3 definiert dazu in der Datei »murmur_test.go« (die Endung »_test.go« ist verpflichtend) eine Funktion »TestLookup()« (auch der Präfix »Test« ist vorgeschrieben). Diese Funktion ruft den Konstruktor »NewMurmur()« auf und bindet in der zurückkommenden Objektstruktur (eigentlich ein Pointer darauf) mit »WithFilePath()« die im Testdatenverzeichnis »data/« liegende YAML-Datei ein. Dort steht, wie in Listing 4 zu sehen, ein Eintrag zum Schlüssel »foo«, der auf den Wert »bar« zeigt.
Listing 4
murmur.yaml
foo: bar some-key: "Quoted!"
Genau dieses Ergebnis prüft das Testprogramm aus Listing 3 in Zeile 13. Klappt alles, tut es nichts. Tritt ein Fehler auf, meldet es diesen, und das Go-Test-Framework »testing« gibt auf »t.Fail()« hin die mit »t.Log()« abgesetzten Meldungen zum Einkreisen des Fehlers aus. Abbildung 4 zeigt den Erfolgsfall im Verbose-Modus. Ohne »-v« liefe die Testsuite im Erfolgsfall wortlos durch.
Schon ein simpler Test ist viel besser als gar keiner, und der Paketentwickler kann nach zukünftigen Änderungen ruhigen Gewissens das neue Release ausrollen, sofern die Testsuite noch klaglos durchläuft. Was braucht eine Go-Library auf Github sonst noch – Dokumentation vielleicht? Nichts ist nerviger, als auf Github auf ein interessantes Go-Paket zu stoßen, dessen Autor zu faul war aufzuzeigen, wie es im Detail funktioniert.
Go macht es dem Faulen einfach: Kommentarzeilen direkt über Typdefinitionen oder Funktionen interpretiert es als Dokumentation und zeigt diese auf Wunsch an, mitsamt der automatisch aus dem Quellcode extrahierten Programmstrukturen. Da fruchten keine Ausreden mehr!
Stößt ein Suchender allerdings auf Github auf ein entsprechendes Paket, liegt es noch nicht lokal vor. Daher funktioniert der sonst zur Sichtung der Dokumentation übliche Aufruf von »go doc« auf der Kommandozeile noch nicht. Deshalb sollte einem Go-Projekt auf Github immer eine Datei »README.md« (im Markdown-Format) beiliegen, die neugierigen Besuchern die Nutzung des Pakets möglichst appetitanregend an einem Beispiel erklärt (Abbildung 6).
Bekanntmachung
Außerdem indiziert die Website »pkg.go.dev« alle Pakete auf Github, die nach Go aussehen, und dröselt autogenerierte Manual-Seiten detailliert auf. Um dem Server auf die Sprünge zu helfen, nutzt manchmal ein kurzer Besuch auf https://pgk.go.dev/github.com/user/repo. Allerdings besteht die Website darauf, dass dem Projekt eine gültige Lizenz in Form einer »LICENSE«-Datei beiliegt; »go-murmur« enthält dazu eine Kopie der Apache-2.0-Lizenz. Mit Lizenz formatiert die Doku-Seite die Typen und Funktionen des Pakets für Endnutzer (Abbildung 7), ohne Lizenz meldet sie einen Fehler (Abbildung 8).

Abbildung 7: http://pkg.go.dev hat das Paket indiziert.

Abbildung 8: Die Lizenz muss stimmen, damit http://pkg.go.dev das Paket indiziert.
Zur Nutzung des Pakets in anderen Projekten muss das Github-Repo eine Datei »go.mod« enthalten. Die in Abbildung 9 gezeigte Kommandosequenz erzeugt sie für den Autor des Pakets. Neben dem Modulnamen mit dem vollen Github-Pfad listet die neue Datei »go.mod« unter dem Stichwort »require« alle Pakete, von denen das Modul abhängt. Im vorliegenden Fall benötigt »go-murmur« noch das YAML-Paket, das auf »gopkg.in« liegt. Mit dieser Definition erzeugt der Go-Compiler später einen Dependency Tree, holt die zum Binden des Binarys benötigten Pakete der Reihe nach aus dem Netz und bindet sie ein.
Damit Anwender das neue Paket nutzen können, muss zusätzlich die aktuelle Version des Repos auf Github mit »git tag« ein Tag im Format »v1.2.3« erhalten. So weiß »go mod tidy« auf der Client-Seite, welche Version gerade verfügbar beziehungsweise lokal installiert ist.
Übrigens bietet Github auf der Homepage jedes Repos noch die Option, bestimmte Versionen als Release zu markieren. Die spielen allerdings für Go keine Rolle, der Compiler schaut immer nur nach den Git-Tags im Repo. Auch die Konstante »Version« in Zeile 9 von Listing 2 dient nur der internen Projektverwaltung und interessiert den Go-Compiler nicht.
Catch-22
Aber funktioniert ein neues Programm auch bei anderen Nutzern? Bevor Änderungen auf Github erschienen sind, kann ein Testprogramm sie nicht von dort herunterladen. Da beißt sich die Katze in den Schwanz. Ein »go mod tidy« zur Klärung der Abhängigkeiten findet auf Github kein »go-murmur«, wenn es dort noch nicht hochgeladen wurde. Auch bei neuen Veröffentlichungen findet »go mod tidy« noch die alte Version auf Github und zimmert sie in »go.mod« fest. Das wiederum bewegt ein nachfolgendes »go build« dazu, die alte Version statt der geplanten neuen Ausgabe zu testen.
Abhilfe schafft hier temporär das Schlüsselwort »replace« in »go.mod«, wie Abbildung 10 zeigt. Auf der rechten Seite nach dem »=>« steht »..«. Damit sieht der Compiler später nicht auf Github nach oder verwendet gar eine eventuell bereits heruntergeladene Github-Version des Pakets, sondern sucht lokal im Verzeichnis »..« danach. Dort befindet sich hoffentlich die aktuelle Go-Datei der neuen Version.
Mittelsmann ausschalten
Wer mit »git push« Änderungen am Quellcode auf Github vornimmt, darf nicht erwarten, dass externe Clients wie der Go-Compiler diese sofort mitbekommen. Es gilt vielmehr, sich auf lange Wartezeiten einzustellen, da hier mehrere Caching-Ebenen ihren Dienst tun. Es kann schon mal ein halbes Stündchen dauern, bis alle Änderungen durchgesickert sind.
So kommt es vor, dass »go mod tidy« die neue Version auf Github nicht findet, sondern auf der alten, lokal installierten beharrt. Das liegt oft daran, dass der Go-Compiler »go« Github nicht direkt kontaktiert, sondern über einen in der Environment-Variablen »GOPROXY« gesetzten Mittelsmann.
Dieser Service wird von Googles Go-Team betrieben und soll verhindern, dass Millionen laufende Go-Builder Github mit sich wiederholenden Anfragen überlasten. Während der Entwicklung neuer Versionen gilt es deshalb, den Mittelsmann auszuschalten. Dazu dient das Bestücken der Umgebungsvariablen mit »GOPROXY=direkt«, was den Go-Compiler zwingt, direkt auf Github nach neuen Versionen zu suchen und sich nicht auf den Proxy mit seiner veralteten Information zu verlassen.
Ein weiterer Trick, um den puffernden Proxy bei »go mod tidy« auszuschalten, besteht darin, die in »go.mod« aufgelistete Version manuell hochzusetzen. Dann versucht »go mod tidy« sofort und ohne Cache, an die neuere Version zu kommen.
Hoch damit!
Funktioniert alles nach Vorschrift? Dann ist es Zeit, die Dateien mit »git push« ins Repo auf Github hochzupumpen. Die Version des neuen Releases setzt »git tag« in Abbildung 11 lokal. Ein anschließendes »git push –tags« schiebt das Tag auch ins Repo auf Github. Ab dann stürzen sich hoffentlich unzählige Nutzer auf den Code. Verantwortungsvolle Paketautoren halten ihre Fans bei der Stange, indem sie ab diesem Zeitpunkt keine inkompatiblen API-Änderungen mehr einbringen, eventuell gemeldete “Issues” blitzartig richten und sich artig bedanken. (uba)
Infos
- Snapshot: Mike Schilli, “Kanalarbeiter”, LM 01/2024, S. 78, https://www.lm-online.de/48789














