hetzel.net

Mein Go + SQLite-Stack

Go Gopher Races Past PHP Elephant

Fast alle meine neueren Websites und Dienste laufen auf einem sehr ähnlichen Stack. Schon vor den LLM-Agents habe ich das so ähnlich von Hand geschrieben. Für die Sprechkabine und einen der älteren Merchandise-Shops zum Beispiel. Die Uptime messen wir hier in Jahren.

Go

Eine sehr kleine, stabile, leicht zu verstehende, leicht zu schreibende und leicht zu lesende Sprache. Einzige zwei Hürden, die man als PHP-Geschädigter vielleicht noch zweimal nachlesen muss: Was sind Pointer, was sind Channels? Go ist Garbage Collected, man muss sich in der Regel jenseits von Optimierungen oder Sonderfällen nicht weiter um den Speicher kümmern.

Der Trick gegen die Apache/PHP-Monokultur: Die Go-Standardbibliothek bringt ihren eigenen HTTP-Server mit. Entweder lässt man Go direkt Let's Encrypt und TLS handlen, oder man stellt einen anderen Reverse Proxy davor, der sich darum kümmert. Nginx oder Caddy bieten sich an.

Die Sprache kann wie ein Skript ausgeführt werden oder in ein Binary kompiliert werden. Die Runtime kommt immer mit, das ganze Programm ist in der Regel komplett aus einer kleinen Datei lauffähig.

Man definiert sich sein Datenmodell, seine Routen, seine Handler, kann mittels Middleware Auth und Logging und was auch immer in den Pfad der Requests hineinbauen.

Die Standardbibliothek ist für das Web gemacht. Praktisch alle gebräuchlichen Protokolle und Standards finden sich in abgehangenem, gepflegtem Code wieder. Jenseits des Webs redet Go mit allem an C-Code und mit dem Betriebssystem und weiteren CLI-Tools. Lokal habe ich auch Skripte, die man vielleicht sonst in Shell oder Python schreiben würde, in Go.

Go kommt aus dem Hause Google, ist praktisch ewig rückwärtskompatibel, wird aktiv gepflegt und ist bei dutzenden Riesenfirmen im Einsatz. Sollte Google dichtmachen, wird die Sprache relativ sicher weitergepflegt.

SQLite

Neben Curl wahrscheinlich das rundeste Stück Software, das wir in den letzten 40 Jahren zustande gebracht haben. Wenn es für Flugzeuge und den Mars-Rover gut genug ist, reicht es wohl für mein Blog.

SQLite ist kein separater Datenbankserver, sondern wird in das Binary mit hineinkompiliert oder als Systembibliothek vom Betriebssystem gelinkt.

SQLite kommt entweder über die C-Schnittstelle in Go (CGO) oder man nutzt einen Go-Port wie modernc.org/sqlite, mit eigenen Bugs und Abwägungen, dafür ohne die zusätzliche Dependency des C-Compilers und längere Build-Zeit.

Für die meisten Websites braucht man so: Das Binary, die Datenbankdatei und vielleicht noch eine Environment-Datei mit Schlüsseln und Konfiguration.

Templates

Die Go-Templates sind ein wenig unhandlich. Man schmeckt die Herkunft von Go: Alte C-Compiler-Hasen, die sich und uns das Leben gerne schwer machen. Mit etwas Hilfe vom Chatbot komme ich inzwischen damit klar, aber es ist schon etwas unhandlich.

Etwas näher an PHP ist Templ. Noch nicht selbst probiert, aber deutlich menschenlesbarer.

Wenn Performance eine Rolle spielt: Go ist nah dran an C, und fprintf ist zwar völlig unlesbar, aber dafür sehr sehr schnell für den Hot Path der Anwendung. Hier zum Beispiel die Podcast-Feeds, die nonstop gehämmert werden.

Templates und andere statische Assets lassen sich leicht in das Binary hineinbacken und bleiben mit dem Commit des Codes konsistent.

Frontend

Vanilla JS, etwas Bootstrap, was das Herz begehrt. Server Side Rendering entfernt viel State und Bugs und Schmerzen aus dem Browser. Für etwas Interaktivität kann man sich HTMX ansehen, was über einfache HTML-Attribute Teile von Websites durch neue Elemente vom Server ersetzt und nur 14kb wiegt.

Deployment

Diverse Möglichkeiten von Blech bis Docker. Ich habe jetzt für die meisten Dienste einen Workflow mit git push prod main gewählt. Ein Bare Repo auf dem Server empfängt den neuen Code, triggert einen neuen Build, ein Backup, speichert das vorherige Deployment für einen Rollback und checkt, ob das neue Binary korrekt in systemd startet.

Wer es spannend mag: Go kann als CGI-BIN laufen, like it's 1999, oder als WASM in Cloudflare Workers. Whatever floats your boat.

Backups

SQLite hat diverse Möglichkeiten, einen konsistenten State als Backup zu ziehen. Dieser State wird beim Deploy weggeschrieben und für einen potentiellen Rollback werden bei mir z.B. 7 Stück aufgehoben.

Auf dem Server kann weitere Backup-Software wie Restic oder Rsync weitere konsistente Backups von Daten und Code wegschaufeln.

Scalability

Solange wir hauptsächlich aus SQLite lesen, kein Problem. Sobald wir mehr schreiben, oder größere Transaktionen in der Datenbank fahren, könnte der Single Writer von SQLite problematisch werden. Dann ist aber das Upgrade auf einen "richtigen" Datenbank-Server auch nicht weit. Das database/sql Interface hilft uns weiter.

Performance

Mit pprof lässt sich präzise bestimmen, womit der Code seine Zyklen verschwendet. Die entscheidende Optimierung beim Push-Server für iOS war beispielsweise die schlechte Serialisierung von JSON per Reflection. Kaum gibt man die Typen explizit an, läuft der Code doppelt so schnell. Das war bei einer Handvoll Usern. Jetzt sind es mehrere Tausend, und der Code ist gleich schnell.

Alle meine drölf Go-Dienste laufen jetzt auf zwei von den kleinsten VMs bei Hetzner für je 5 Euro oder so. Ohne nennenswerten RAM- oder CPU-Verbrauch. Für den alten Apache/PHP-Stack war es ein Vielfaches davon, nur für Bits und so alleine.

LLM

Die Portierung von WordPress/PHP zu Go für Bits und so ist größtenteils vollautomatisch gelaufen, bis der Output von Go bis auf das Bit identisch zum PHP-Code war. Einziger Haken, der mir auch vorher klar war: Käufe mit externen Diensten sind schlecht zu testen und haben etwas Handarbeit gebraucht.

Der Codex ist auch nur ein Mensch. Lustigerweise rennt er genauso wie ich von Hand immer in die gleichen Fallen mit Go: Unused Imports, Unused Variables. Es gibt natürlich noch mehr Fallstricke, aber in vielen Zehntausend Zeilen ist er mir dort noch nicht reingestiegen. Die etwas wortreiche if err != nil Syntax, die überall gebraucht wird, ist zwar repetitiv, aber ohne großen Interpretationsspielraum. Leicht zu schreiben, leicht zu lesen. Für Menschen, für den Agent.