Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 30 Min.

Programmierung mit Go und HCL

Die HashiCorp Configuration Language (HCL) stellt für DevOps die Spezifikation von Cloud-Konfigurationen in lesbarer Form in den Mittelpunkt.
HashiCorp hat sich mit seinen Open-Source-Produkten zu einem wichtigen Player im DevOps-Markt entwickelt. Inzwischen unterstützen einige wichtige Cloud-Plattformen wie Amazon, Google oder Microsoft direkt deren Produkte. Die zugrundeliegende Konfigurationssprache HCL hat HashiCorp selbst entworfen und mit der Programmiersprache Go realisiert.

Lesbarkeit und Struktur stehen im Mittelpunkt von HCL

Beim Entwurf der Syntax von HCL wurde HashiCorp vor allem von der UCL (Universal Configuration Language) angeregt. Die UCL stammt ursprünglich aus der Unix-Welt; der zugehörige Parser (libucl) diente dort als Grundlage zur Realisierung verschiedener Tools für das Management von Konfigurationsdateien. UCL selbst wurde stark vom Aufbau und der Konfigurationsdatei der Webserver-Software nginx beeinflusst. Konzeptionell strebte HashiCorp mit HCL eine für den Menschen lesbare und gleichzeitig für Maschinen verarbeitbare Form an. Mit HCL gelang HashiCorp eine Balance zwischen guter Lesbarkeit für den Menschen und schneller Verarbeitung von Maschinen.Mit etwas Programmierkenntnissen lässt sich die Syntax von HCL leicht lesen und schreiben. Dies liegt sicherlich am klaren Aufbau und der visuellen Struktur im Vergleich zu anderen bekannten Konfigurationssprachen wie YAML oder JSON. Mit HCL spezifizierte Konfigurationen lehnen sich visuell stark an JSON an, erweitert mit zusätzlichen Datenstrukturen und Built-in-Fähigkeiten. HCL kombiniert die Stärke einer universellen Sprache wie Python oder Java mit der Einfachheit und Lesbarkeit von JSON. Insofern lässt sich ein HCL-Objekt in ein gültiges JSON-Format überführen, so dass die Sprache als JSON-kompatibel gilt.Ursprünglich für die Werkzeuge von HashiCorp konzipiert, entwickelt sich HCL zunehmend zu einer allgemeinen Konfigurationssprache im DevOps-Bereich. Das liegt sicherlich an den Vorteilen von HCL gegenüber dem recht häufig für Konfigurationen eingesetzten JSON oder YAML-Format. Vor allem aber bewirkte die Bereitstellung von HCL für die Open-Source-Gemeinde eine zunehmende Akzeptanz. Mit HCL steht ein frei zugängliches Toolkit für die Verarbeitung individueller Konfigurationsdateien zur Verfügung. Damit parst man eine Konfigurationsdatei, überprüft sie auf Konformität mit den erwarteten Strukturen und erzeugt ein höherwertiges Spezifikationsobjekt, dass die Infrastruktur anschließend weiterverarbeitet.

Golang – eine geeignete Ausgangsbasis für HCL

Bei der Programmiersprache Go (Bild 1), manchmal auch als Golang bezeichnet, handelt es sich um eine Programmiersprache der Open-Source-Gemeinde. Obwohl die Konzeption der Sprache bereits in 2009 von Google der Öffentlichkeit vorgestellt wurde, dauerte es länger bis die erste stabile Version 2012 von Go erschien. Als imperative, kompilierbare Programmiersprache legt Go einen Schwerpunkt auf die Systemprogrammierung speziell Nebenläufigkeit (Concurrent Programming) und Networking. Mit der Konzeption von Go wollte das Google-Team dem Programmierer vor allem eine bessere Unterstützung für Multiprocessing, Netzwerkdienste, Cluster- und Cloud-Computing bieten.
Die Gestaltung des Maskottchenvon Go orientiert sich an Glenda, dem Häschen des Plan 9-Betriebssystems(Bild 1) © Simon
Syntaktisch besitzt Go gewisse Ähnlichkeiten zu C, verfügt allerdings über eine automatische Speicherbereinigung (Garbage Collection). Bei der Konzeption der Syntax stand bei den Entwicklern Lesbarkeit, Benutzerfreundlichkeit, statische Typprüfung und Runtime-Effizienz im Mittelpunkt. Als kompilierte Sprache mit hoher Übersetzungsgeschwindigkeit, verfügt das Go-System zusätzlich über ein leistungsfähiges Remote-Package-Management. Das get-Kommando des Go-Systems lädt über das Internet eine Vielzahl von Packages, sogenannte Moduln, herunter und gewährleistet dabei auch Konformität zu Version-Control-Systemen.Go besitzt insbesondere für die Nebenläufige Programmierung spezielle Features: Leichtgewichtige Prozesse (Go-Routines), Channels (Kanäle) und den select-Befehl. Diese Features, auch Built-in-Facilities für Concurrent Programming genannt, unterstützen nicht nur CPU-Parallelität, sondern bieten auch Asynchronität. Mit asynchroner Ausführung lassen sich Arbeiten im Hintergrund erledigen, solange das Programm selbst andere Aufgaben durchführt. Eine über das Schlüsselwort go aufgerufene Funktion nennt man Go-Routine; sie startet einen leichtgewichtigen Prozess innerhalb desselben Adress-Space die Zugriffe auf gemeinsamen Speicher synchronisiert erfolgen können. Dazu stehen Channels und ein spezielles Modul/Package sync direkt in der Sprache bereit.

WebAssembly für höhere Performance in Web-Apps

Das W3C definiert über den WebAssembly-Standard (Wasm) einen Bytecode für die Ausführung von Programmen innerhalb von Webbrowsern. Wasm unterstützt die Engines aller wichtigen Webbrowser: Mozilla Firefox, Google Chrome, Apple Safari und Microsoft Edge. Neben Go kompiliert auch Rust nativ nach Wasm – für C und C++ benötigt man auf das Tool Emscripten, um C/C++-Quellcode nach Wasm zu kompilieren. Die Ende 2019 von Fastly, Intel, Mozilla und Red Hat gegründete Bytecode Alliance wurde 2021 in eine Non-Profit-Organisation überführt und nahm neue Mitglieder wie ARM, Microsoft oder Siemens auf.
Go-Routinen kommunizieren miteinander über Kanäle (Channels), dabei wird Nebenläufigkeit durch CSP (Communicating Sequential Processes) realisiert. Jeder Channel besitzt einen Typ chan T, so dass dieser Kanal nur Nachrichten vom Typ T verarbeiten kann. Eine Kommunikation erfolgt über das Schreiben (chan <-) in den Kanal oder das Lesen (<- chan) aus dem Kanal. Durch Einsatz eines select-Statements wartet eine Go-Routine auf die Ausführung mehrerer Kommunikationsvorgänge, welche die jeweiligen case-Statements definieren. Sobald eine Kommunikation stattfinden kann, das heißt der case-Zweig für einen Channel erfüllt ist, werden die zugehörigen Anweisungen ausgeführt. Sind mehrere case-Zweige gleichzeitig erfüllt, wählt Go einen zufällig zur Ausführung aus.Die optionale default-Klausel des select-Statements verhindert eine vollständige Blockierung, da Go dessen Anweisungen immer ausgeführt, sollte keiner der case-Zweige zur Ausführung bereit sein. Damit dient das select-Statement als Hilfsmittel für eine nichtblockierende Kommunikation über mehrere Kanäle. Während Go-Routinen die Ausführung übernehmen und Channels die Kommunikation zwischen Go-Routinen durchführen, dient das select-Statement der Koordination indem select auf verschiedene Operationen wartet, diese überwacht und entsprechend ausführt. Alle drei zusammen Go-Routinen, Channels und das select-Statement stellen in Go die Mittel der Wahl dar, um Nebenläufigkeit (Concurrency) zu realisieren.

Installation und Einrichten einer Programmierumgebung für Go

Über die Homepage lädt man die aktuelle Version des Go-Systems für das jeweilige Betriebssystem herunter. Go unterstützt primär die Betriebssysteme Linux, macOS und Windows. Für embedded Systeme und WebAssembly (Wasm) steht TinyGo zur Verfügung. Alternativ kann man EMBD für die Platinen-Computer Raspberry Pi und Beaglebone Black nutzen. Für Roboter, Drohnen oder Internet-of-Things (IoT)-Hardware kann man auf Gobot zurückgreifen. Benötigt man gezielt eine andere, das heißt eine ganz bestimmte Version von Go, so findet man diese zum Download auf der Webseite go.dev/dl/.Nach dem Herunterladen eines Go-Systems erhält man über die Webseite https://go.dev/doc/install nähere Anleitungen für das jeweils vorliegende Betriebssystem. Mit der Ausnahme von Linux richtet ein im Download enthaltenes Installationsprogramm (Bild 2) die Konfiguration des Go-Systems automatisch ein. Über ein Terminalfenster oder Eingabeaufforderung prüft der Befehl go, ob das Go-System, manchmal auch Go-Toolchain genannt, für die Programmierung zur Verfügung steht: Es erscheint eine Liste von Go-Kommandos mit einer Anleitung zum Gebrauch des Go-Hilfesystems. Um diesen Befehl auf der Ebene des Betriebssystems auszuführen, muss in der Pfad-Variable das bin-Unterverzeichnis von Go eingetragen sein.
Das im Download von Goenthaltene Installationsprogramm konfiguriert die Software automatisch(Bild 2) © Simon
Für das korrekte Arbeiten eines Go-Systems benötigt die Go-Toolchain einen sogenannten Go-Workspace – dabei handelt es sich um eine Hierarchie von Verzeichnissen die aus drei Ordnern mit den folgenden Namen besteht:Go-Routinen und Kanäle stellen einfache Hilfsmittel dar, um das Konzept der CSPs (Communicating Sequential Processes) zu realisieren. Bei einer Go-Routine handelt es sich um einen leichtgewichtigen Thread, den nicht wie etwa bei Java das Betriebssystem, sondern das Go-Laufzeitsystem selbst verwaltet.

Channels & Go-Routinen ermöglichen eine automatische Synchronisation

Damit besitzt eine Go-Routine keine große und fixe Größe im Stack, vielmehr ist der zugehörige Thread segmentiert und wächst nur, wenn er es wirklich muss. Das Go-Runtime scheduled die Threads und nicht das Betriebssystem – damit entfällt der Overhead zwischen Go-Runtime und Betriebssystem. Eine neue Go-Routine startet man als Programmierer durch Aufruf einer Funktion zusammen mit dem Go-Schlüsselwort: go function().Ein Kanal dient der Kommunikation zwischen Go-Routinen. Grundsätzlich können Daten in einen Kanal erst gesendet werden, wenn es dafür einen Empfänger gibt. Einen Channel erzeugt der make-Befehl: meinKanal := make (chan int). Mit dem Go-Statement meinKanal <- 4711 schreibt man den Wert 4711 in den Kanal. Steht im Kanal eine Integer-Variable zum Lesen bereit, so empfängt eine Go-Routine diese über den Befehl meineVar := <- meinKanal und weist deren Wert einer neuen Variablen (meineVar) zu. Schreibt eine Go-Routine A Daten in eine Channel, so kann eine andere Go-Routine B die Daten aus dem Channel lesen (empfangen) und verarbeiten – allerdings erst, wenn diese bereitstehen.

Google stellt Beispielprojekte für Go zur Verfügung

Die Webseite Google Open Source (cs.opensource.google) erschließt über eine Reihe von Git-Repositories umfassende Informationen zum Go-System mit Beispielprogrammen. Das Repository example enthält eine Sammlung von Go-Programmen, welche die Sprache, den Einsatz der Go-Toolchain und die Standard-Library veranschaulichen. Speziell decken die Beispielprogramme Text- und HTML-Templates, HTTP-Requests/Respones, Logging, Synchronisation, Unit- und Integrationstests und Einbindung der Google App Engine ab. Zusätzlich umfasst das Repository Quellcode, der den Einsatz des go/types-Packages erläutert. Neben Type-Checking besitzt dieses Package Funktionen zur Analyse und Manipulation von Go-Programmen.
Go kennt ungepufferte und gepufferte Kanäle – einen Channel mit einer Puffergröße von drei Integer-Variablen legt der Befehl: meinBufferedKanal := make( chan int, 3) an. Ein gepufferter Kanal enthält im Unterschied zu einem ungepufferten mehrere Meldungen – meinBufferedKanal entspricht einem Kanal mit einer Kapazität von 3. Oder anders ausgedrückt: Der Kanal hat eine Puffergröße von 3. Standardmäßig erfolgt die Kommunikation über Kanäle synchron. Das Senden (Schreiben in einen Kanal) von Daten wirkt blockierend, bis diese durch Lesen aus dem Kanal empfangen werden, und dies geschieht auch umgekehrt! Bei einem gepufferten Kanal kann ein Absender solange Daten in den Kanal schreiben, bis dessen Puffer voll ist.Aufgrund dieses Sachverhalts bezeichnet man einen Kanal mit einer Pufferlänge größer 0 als asynchrone Pipe. Ein derartiger Kanal bewirkt, dass sich die Datenübertragung des Senders und die Datenerfassung des Empfängers in einem asynchronen Zustand befinden. Die vom Sender gesendeten Daten werden in den Puffer gestellt und warten darauf, dass der Empfänger die Daten erhält/liest, das heißt verarbeitet. Solange der Puffer nicht voll ist, blockiert Go nicht den Sendevorgang, der Daten in den Kanal schreibt. Und umgekehrt: Bevor der Puffer gelesen wird, wird der Empfangsvorgang, der die Daten aus dem Kanal liest/empfängt, nicht blockiert.

Vielfältige Umgebungsvariable für unterschiedliche Einsatzzwecke

Grundsätzlich kennt ein Go-System eine sehr große Anzahl an Umgebungsvariablen, die der Befehl go env einzeln auflistet (Bild 3). Achtung: Die in der Liste ausgegebenen Werte stellen alle Standard-, das heißt Vorgabewerte für das Go-System dar; diese verwendet die Go-Toolchain – auch wenn keine explizite Definition dieser Umgebungsvariable über das Betriebssystem vorliegt! Die bekannteste von ihnen ist die Umgebungsvariable GOROOT, die das Installationsverzeichnis eines Go-SDKs referenziert. Bei der Einrichtung eines Go-Systems für Windows erfolgt die Installation des Go-SDKs standardmäßig in das Verzeichnis C:\Go. Unter einem Linux- oder macOS-System verwendet das Installationsprogramm den Ablageordner /usr/local/go.
Der Befehl go env -jsongibt alle oder ausgewählte Umgebungsvariable des aktuellen Go-Systems im JSON-Format aus(Bild 3) © Simon
Benötigt man für die Programmierung verschiedene Go-Systeme, so aktiviert man über GOROOT die jeweils gewünschte Version. Der Befehl go version zeigt die Versionsnummer des aktuell zugänglichen Go-Systems an. Um das Wurzelverzeichnis für einen Go-Workspace selbst zu setzen, greift man auf die Umgebungsvariable GOPATH zurück. Über diese Umgebungsvariable des Betriebssystems gibt man einen beliebigen Ordner als Root-Verzeichnis für den Go-Workspace der Go-Toolchain vor. Als Standardwert verwendet jedes Go-System als Wurzelverzeichnis für einen Go-Workspace unter Windows den Ordner: %USERPROFILE%\go und unter einem Unix-System (Linux, macOS) den Ordner: $HOME/go.Zusätzlich kennt ein Go-System noch die Umgebungsvariable GOBIN – sie legt fest in welches Verzeichnis das Go-System die ausführbare Anwendung ablegt. Solange GOPATH nicht gesetzt ist, verwendet Go standardmäßig für GOBIN $HOME/go/bin (%USERPROFILE%\go\bin); ansonsten greift das Go-System auf $GOPATH/bin ($GOPATH\bin) zurück. Befindet sich die GOBIN-Umgebungsvariable in der Pfad-Definition des Betriebssystems, so lässt sich eine Go-Anwendung durch Eingabe des einfachen Namens der ausführbaren Datei starten. Ansonsten muss man vor dem Dateinamen zusätzlich noch ihren vollständigen Pfad angeben.

Versionsmanagement für Go-Modulen

Die Go-Toolchain unterstützt den Entwickler bei der Aktualisierung der Abhängigkeiten eines Moduls. Alle verfügbaren Minor- und Patch-Upgrades für alle direkten und indirekten Abhängigkeiten zeigt der Befehl go list -u -m all an. Eine Abhängigkeit samt ihren Unterabhängigkeiten aktualisiert der Befehl go get -u Packagename. Eine Aktualisierung auf die aktuelle Minor- oder Patch-Release für ein Modul führt der Befehl go get -u durch. Um die Abhängigkeiten eines Moduls auf das aktuell Patch-Release zu aktualisieren, kommt der Befehl go get -u=patch zum Einsatz. Alle Packages im GOPATH aktualisiert der Befehl go get - u all.
Die Go-Toolchain unterstützt einen Cross-Compile für verschiedene Betriebssysteme und Prozessor-Architekturen. Die Auswahl des Betriebssystems und der Prozessor-Architektur steuert der go build-Befehl über die beiden env-Umgebungsvariablen GOOS für das Betriebssystem und GOARCH für die Prozessor-Architektur. Zum Beispiel für macOS und eine 64-Bit-Architektur lautet der Befehl: GOOS=darwin GOARCH=amd64 go build. Fehlt die Angabe dieser beiden Umgebungsvariablen, so verwendet der Compiler die in GOHOSTARCH und GOHOSTOS stehenden Default-Werte.Eine Liste aller für das installierte Go-System verfügbaren Betriebssysteme/Prozessor-Architekturen gibt der Befehl go tool dist list aus. Dabei bedeutet ein Listeneintrag android/arm64, dass eine Kompilierung für das Betriebssystem Android mit der Prozessorarchitektur arm64 möglich ist. Wobei ein Cross-Compile für Android eine Installation des Android NDK und weitere Vorkehrungen erfordern. Mit der Go-Bibliothek runtime ermittelt man schnell über die beiden Konstanten runtime.GOOS und runtime.GOARCH auf welchem Betriebssystem und welcher Prozessor-Architektur eine Anwendung zur Ausführung kommt.

Go gewährleistet Typsicherheit und kennt alle gängigen Datentypen

Da Go eine Typsicherheit durch statische Typisierung realisiert, besitzen alle Variablen während der Programmlaufzeit immer einen spezifischen, nicht-veränderbaren Datentyp. Diese Typsicherheit schließt vielfältige Fehlerquellen schon während der Codierung des Programms aus. Datentypen in Go unterteilen sich in vier verschiedene Kategorien; wobei es sich im Unterschied zu den Basistypen bei den anderen drei um sogenannte Built-in-Datentypen handelt:Zahlen unterteilen sich in drei weitere Unterkategorien: Integer, Gleitkomma und komplexe Zahlen. Integertypen lassen sich als signed (int) oder unsigned (uint) (mit oder ohne Vorzeichen) deklarieren – beide stehen in vier verschiedenen Größen für die Genauigkeit zur Verfügung. So stellt int64 eine mit Vorzeichen versehene Ganzzahl in 64-Bit-Größe dar oder uint8 eine 8-Bit große Ganzzahl ohne Vorzeichen. Im Unterschied dazu besitzt der Gleitkomma-Datentyp nur zwei Unterkategorien: float32 und float64 also eine Gleitkommazahl mit 32- oder 64-Bit-Größe. Ebenso die komplexen Zahlen: complex64 und complex128 – erstere besteht aus einem jeweils float32 beziehungsweise float64 bestehenden Real- und Imaginärteil.Ein struct (Record) deklariert über das type-Schlüsselwort einen neuen Datentyp, das anschließende struct-Konstrukt legt die einzelnen Felder der Satzstruktur in einem von geschweiften Klammern umschlossenen Block fest. Größe und Typ der in einem Array enthaltenen Elemente spezifiziert eine in eckigen Klammern genannte Zahl. Drei Punkte [...] in eckigen Klammern signalisieren, dass der Compiler das Zählen der nachfolgenden Elemente des Arrays übernehmen soll. Ein Ausschnittstyp (Slice) stellt ein Segment eines Arrays dar – beide sind indiziert und haben eine Länge. Die Deklaration eines Slice erfolgt entweder über ein Array ohne Größenangabe oder mit der make-Funktion: make([ ] int8, 5, 12); 5 entspricht der Länge und 12 der optionalen Kapazität.Ein Zeiger (Pointer) stellt eine Variable dar, deren Wert die Adresse einer anderen Variablen, Funktion oder Konstante entspricht. Analog C repräsentiert ein *-Zeichen gefolgt von einem Datentyp einen Zeigertyp (Pointer): var xZgr *datentyp. Alternativ erzeugt die Built-in-Funktion new(datentyp) einen Zeiger auf den übergebenen Datentyp. Das *-Zeichen (Dereferenzoperator) liefert den Inhalt der Variable; initialisiert oder verändert den über den Zeiger erreichbaren Wert (var *xZgr=10). Um die Adresse dieser Variablen im Hauptspeicher zu erhalten, verwendet man das &-Zeichen (Adressoperator): &xZgr liefert eine Adresse auf ein Objekt zurück. Im Unterschied zu C oder C++ kennt Go keine Zeigerarithmetik, was Fehler beim Rechnen mit Zeigern auf Basis der Speichergröße verhindert.
Eine Map besteht aus einer Sammlungvon Elementen, die in Schlüssel-Wert-Paare gruppiert sind: Jeder Schlüssel verweist auf einen einzigen Wert(Bild 4) © Simon
Eine Map, manchmal auch Dictionary, Hash Table oder Assoziativ-Speicher genannt, stellt eine unsortierte Sammlung von Schlüssel-Wert-Paaren (Bild 4) dar. Das Schlüsselwort map gefolgt von einem in eckigen Klammern stehenden Datentyp und einer weiteren Typangabe definiert eine Map: var x map[string]int. Im Beispiel entspricht x einer Map von Zeichenketten, die auf ganze Zahlen verweisen, aber keinen Speicherbereich reserviert – während var x := make(map[string]int) den erforderlichen Speicherbereich für die Map im Voraus anlegt. Zugriff auf die Elemente einer Map erfolgt über deren Schlüsselname in eckigen Klammern: x[„Zeichenkette“] liefert den dieser Zeichenkette zugeordneten Wert zurück.

Go enthält ab Version 1.11 ein eigenständiges Modulsystem

Ein Package gruppiert zusammengehörenden Quellcode zu einer Einheit, um ihn wiederverwenden zu können. Jede Quellcode-Datei enthält am Anfang eine Package-Deklaration – deshalb gehört grundsätzlich jede .go-Datei zu einem Package. Auf das Schlüsselwort package folgt dessen Name; er sollte aus einem einfachen Substantiv bestehen; dabei sind nur Kleinbuchstaben erlaubt. Package-Name und der Ordner-Name müssen übereinstimmen. Um ein Objekt eines Package nach außen bekannt zu geben, muss dessen Namen mit einem Großbuchstaben beginnen. Am Schlüsselwort main beim Package-Name erkennt der Go-Compiler, dass er dieses anstatt in eine Library in ein ausführbares Programm überführen soll.Unter einem Modul versteht man eine Sammlung von Go-Packages mit Abhängigkeiten und Versionsunterstützung. Alle Packages eines Moduls befinden sich in einer Dateihierarchie, deren Wurzel eine go.mod-Datei enthält. Bei der Vergabe der Versionsnummern kommt Semantic Versioning (Major.Minor.Patch) zum Einsatz. Alle Abhängigkeiten eines Moduls gibt der Befehl go list -m all aus. Alle einem Go-System zugänglichen Packages zeigt der Befehl go list … an. Der Befehl go list -f “{{.ImportPath}} {{.Imports}}“ ./... erzeugt eine Liste aller in einem Modul importierten Packages.Mit Go 1.11 wurden Module erstmals eingeführt, aber erst die Version 1.16 verwendet automatisch das Modulsystem beim Build von Packages über den Go-Befehl. Setzt man jedoch die Umgebungsvariable GO111MODULE auf off, so greift der go-Befehl beim Build auf das GOPATH-Verzeichnis zurück. Die Zahlenfolge 111 im Namen dieser Umgebungsvariable soll an die Einführung des Modulsystems mit Go 1.11 erinnern. Das Modulsystems erlaubt erstmals in Go programmierte Projekte außerhalb des GOPATH-Verzeichnisses. Damit verbessert und erleichtert sich das Package-Management und bietet mehr Flexibilität.

Go besitzt flexibles Modulsystem für Versionen und Moduln

Der Inhalt einer go.mod-Datei definiert den Importpfad des Moduls und seine speziellen Abhängigkeiten. Bis zur Version 1.15 von Go enthält GO111MODULE den Wert auto. Diese Vorgabe behandelt nur ein Verzeichnis mit einer go.mod-Datei als ein Modul. Um das Go-Modulsystem zu aktivieren, setzt man die Umgebungsvariable GO111MODULE=“on“. Das Ändern des GO111MODULE-Werts erfolgt auf Ebene des Betriebssystems über den export- (Linux, macOS) oder set-Befehl (Windows). Der -w-Schalter beim go env-Befehl ändert eine Umgebungsvariable wie GO111MODULE permanent: go env -w GO111MODULE=“on“.Für Go gibt es keine zentrale Package-Registry wie man sie über NPM aus der JavaScript-Welt kennt. Go installiert Packages auch nicht wie npm für JavaScript in den zentralen node_modules-Ordner eines Projekts, vielmehr erfolgt die Ablage aller mit Go ausgelieferten Packages in den GOROOT/src-Ordner, alle anderen projektspezifischen Abhängigkeiten in das pkg-Verzeichnis von GOPATH. Für die Installation eines Packages unterstützt Go standardmäßig die folgenden Change-Configuration-Management-Systeme (CCM): Bazaar, Fossil, Git, Mercurial und Subversion. Go cloned das entsprechende CCM-Repository innerhalb des $GOPATH/pkg-Ordners. Dabei installiert Go Packages von folgenden Quellcode-Hosting-Sites: Bitbucket, GitHub, Launchpad und IBM DevOps Services.Die Go-Version 1.5 führte im Projektordner ein vendor-Verzeichnis ein, das ab Version 1.6 in den Standard übernommen wurde. Stellt der Build-Prozess die Existenz eines vendor-Verzeichnisses fest, gibt er den dortigen Packages Vorrang gegenüber den Packages aus dem GOPATH. Mit dem Befehl go mod vendor legt man ein vendor-Verzeichnis an und kopiert alle in der go.mod-Datei spezifizierten Abhängigkeiten in dieses neue Verzeichnis. Das vendor-Verzeichnis in Go entspricht konzeptionell dem zentralen node_modules-Ordner in JavaScript.Innerhalb der go.mod-Datei deklariert man verschiedene Arten oder Formen der Abhängigkeiten eines Moduls über die Schlüsselworte: require, exclude und replace. Alle von dem Modul als Voraussetzung benötigten Packages nimmt man in eine require-Liste auf. Über die exclude-Liste schließt man bestimmte Versionen eines Moduls aus. Um ein in der require-Liste genanntes Modul durch ein anderes zu ersetzen, trägt man dieses in die replace-Liste ein. Dies verdeutlicht die hohe Flexibilität und zielorientiere Ausrichtung des Go-Modulsystems.

Standardbibliothek von Go für alle unterstützten Plattformen verfügbar

Das Go-System umfasst eine Standardbibliothek mit einer Reihe von Packages, welche die Sprache verbessern und erweitern. Diese Packages erlauben es jedem Entwickler, bereits in Go realisierte Funktionen zu nutzen, ohne dafür eigene Packages programmieren oder von anderen Programmierern bereitgestellte Packages herunterladen und einsetzen zu müssen. Die in der Standardbibliothek von G enthaltenen Packages sind direkt mit der Programmiersprache verknüpft. Diese unmittelbare Verbindung eines Go-Systems mit ihrer Standardbibliothek gewährleistet immer deren Aktualität, Rückwärtskompatibilität und Fehlerfreiheit. Daher sollte man grundsätzlich in der Entwicklung die Go-Standardbibliothek einsetzen, da sie eine hohe Zuverlässigkeit bietet.Um einen besseren Überblick zu den Features der Standardbibliothek zu erhalten, hat das Go-Team die darin enthaltenen mehr als 150 Packages in fast 40 Kategorien unterteilt. Die Homepage der Go-Standardbibliothek dokumentiert alle Kategorien mit den zugeordneten Packages und listet diese alphabetisch auf. Zusätzlich hinterlegt die Homepage den Einsatz der Packages mit konkreten Code-Beispielen und bietet eine Suchfunktion nach Symbolen (Konstante, Variable, Funktion, Typ). Den zugehörigen Quellcode der Standardbibliothek befindet sich im Verzeichnis $GOROOT/src einer Go-Installation. Die vorübersetzten Dateien aller Packages der Standardbibliothek aller installierten Zielplattformen und Betriebssysteme enthält der $GOROOT/pkg-Ordner (Bild 5) des Go-Systems.
Ein Ausschnitt des pkg-Ordnerseiner Go-Installation mit der vorübersetzten Standardbibliothek(Bild 5) © Simon
Die große Anzahl an Packages in der Standardbibliothek ermöglicht keine detaillierte Vorstellung – es bietet sich eine Auswahl nach ihrer Bedeutung in der Praxis an. Zunächst hilft eine Unterteilung in die beiden Gruppen: Core-Packages und Packages für die Netzwerkprogrammierung weiter. Zu den Core-Packages sollen alle Packages gehören, die man häufig bei der Entwicklung von Go-Programmen benötigt. Zu diesen Packages oder Kategorien gehören in alphabetischer Reihenfolge: container, encoding, errors, fmt, io, log, math, os, path, sync, sort, strings oder time. Die Packages für die Netzwerkprogrammierung unterstützen die Client-Server-Kommunikation mit den Protokollen TCP, HTTP und RPC – die zugehörigen Funktionen befinden sich in der net-Kategorie.Um ein bestimmtes Package in einem Projekt zu nutzen, macht man es dem installierten Go-System über den Befehl go get zugänglich; dieser setzt die Installation eines Git-Clients voraus. Über den -u-Flag von go get installiert man auch alle abhängigen Packages. Zudem aktualisiert der go get-Befehl die Modulabhängigkeiten des Go-Programms, die sich in der go.mod-Datei befinden. Als Quelle greift der go get-Befehl auf die Umgebungsvariable GOPROXY zurück, die standardmäßig https://proxy.golang.org referenziert. Die dortigen Packages erkundet man über die Webseite https://pkg.go.dev/ – einer zentrale Informationsstelle über Go-Packages und Module.Alle zur Standard-Library gehörenden Packages benötigen keine separate Installation, da sie direkt mit dem Go-System ausgeliefert werden. Dem go get-Befehl kann man aber auch die Quelle über den Namen des Packages mitteilen. Befindet es sich im Web, so verwendet man beim Package-Namen eine URL ohne Angabe des HTTP-Protokolls. Für ein Package auf GitHub mit dem Namen xxxname in der Version vnr, lautet der Befehl: go get github.com/username/xxxname/vnr. Der Befehl installiert dieses Package im gleichnamigen Verzeichnis des $GOPATH/pkg-Ordners. Für private Repositories muss man zum einen die Umgebungsvariable GOPRIVATE setzen und zum anderen entweder Secure Shell (SSH)-Keys oder Personal Access Token (PAT) nutzen.Um ein eignes Modul für die Entwicklung mit Go anderen Projekten bereitzustellen, legt man anfangs das Repository und den Namen des Moduls in einem CCM-System (zum Beispiel ein Git-Repository) an und macht dem CCM-Client das Remote-Repository bekannt. Danach erzeugt man die go.mod-Datei für das Modul vor der Installation eventueller Abhängigkeiten. Dazu setzt man den Befehl go mod init modulname im Wurzelverzeichnis des Modul-Projekts ein. Der mod init-Befehl legt im Projektverzeichnis eine go.mod-Datei für das Modul mit dem Dateiname modulname.go an, trägt darin das Repository und den Namen des Moduls (den Ablageort) bei der module-Angabe und die Versionsnummer des bei der Programmierung verwendeten Go-Systems bei der go-Angabe ein.Die go.mod-Datei entspricht in der JavaScript-Welt der package.json-Datei; beide enthalten den Import-Pfad des Moduls und die von seinen Packages benötigten Abhängigkeiten. Anhand der module-Angabe erhält ein Entwickler, der das Modul verwenden will alle notwendigen Informationen für den im Quellcode seines Programms benötigten import-Befehl. Eventuelle Abhängigkeiten des Moduls führt Go beim require-Befehl einzeln in einer Liste auf. Um ein Modul über GitLab anderen Programmierern zugänglich zu machen, orientiert sich das Projekt-Verzeichnis des Modul-Projekts daran: gitlab.com/go-module-projekt. Ein in diesem Verzeichnis ausgeführter Befehl go mod init gitlab.com/go-module-projekt/1 für Version 1 des Moduls mit anschließender Installation einiger abhängigen Packages enthält in der go.mod-Datei:

module gitlab.com/go-module-projekt/1
go 1.17
require (
    github.com/abhaengig1/package1
    golang.org/abhaengig2/package2
    gopkg.in/abhaengig3/package3
…
)
 
 
Bei der Konzeption von Go legten die Entwickler vor allem Wert darauf, die Komplexität der Sprache möglichst gering zu halten. Daher fanden die besten Charaktereigenschaften einer Vielzahl moderner Sprachen (C, JavaScript, Oberon, Python) Eingang in Go. Grundsätzlich erleichtert das Go-System wesentlich die Wiederverwendung von Code. Zusätzlich hat Go die gängige Art und Weise der objektorientierten Entwicklung überarbeitet. Wegen der besonderen Bedeutung von Effizienz und Performance für Go haben die Entwickler ein einfaches und effektives Typsystem entworfen, ohne dass der durch die Objektorientierung verbundene (und häufig umstrittene) Overhead anfällt. Go kennt keine direkten Sprachkonstrukte für Klassen, Vererbung und somit auch keine inhärente Klassenhierarchie.

Go ermöglicht Objektorientierung erzwingt sie aber nicht

Um Objekte/Klassen innerhalb eines Programms zu realisieren, greift man in Go auf die beiden Schlüsselwörter type und struct zurück. Eine Klasse Person beispielsweise deklariert man wie folgt:

type Person struct {
    nachname string
    vorname string
    alter int
}
 
 
Allerdings kann man nicht wie in objektorientierten Sprachen üblich Methoden innerhalb des type … struct des Objekts/Klasse programmieren, vielmehr muss man diese über ausgelagerte Funktionen im zugehörigen Package realisieren. Dazu definiert man mit dem Schlüsselwort func eine Funktion, der man als Empfänger/Receiver eine Instanz des Objekts (hier: Person) übergibt. Sollen ein oder mehrere Attribute eines Objekts verändert werden, so muss der Receiver als Zeiger also func (p *Person) erhoeheAlter() übergeben werden:

// Datei person.go - Deklaration der Klasse "Person"
package person
type Person struct {
      nachname string
      vorname string
      alter int
}
func NewPerson(nachname string, vorname string, alter int) Person {
      person := Person{nachname, vorname, alter}
      return person
}
func (p *Person) erhoeheAlter() {
      p.alter = p.alter + 1
}
func (p Person) GebeDatenaus() string {
      return p.vorname + " " + p.nachname
}

 
Man spricht von einem Pointer Receiver und bei der Funktion von einer Receiver Function. Zur Kapselung der Daten nutzt man die Konvention von Go, dass alle mit einem Großbuchstaben beginnenden Symbole als public und alle mit einem Kleinbuchstaben beginnenden Symbole als private gelten.Beispielsweise in einem Package person mit obiger Typ-Deklaration und zwei Funktionen GebeDatenaus() und erhoeheAlter() ist die erste Funktion wegen des Großbuchstabens am Anfang des Namens öffentlich und die zweite erhoeheAlter() aufgrund des anfänglichen Kleinbuchstabens nur privat und damit nicht öffentlich zugänglich. Das heißt das Package person kapselt die Attribute der Klasse Person, indem diese nur über die Methoden der Klasse für Manipulationen sichtbar sind. Damit erfüllt Go das Geheimnisprinzip (Information Hiding) der Objektorientierung.Vererbung als Ist-Ein/Eine-Beziehung (Subclassing) unterstützt Go nicht – allerdings das Prinzip Komposition anstelle von Vererbung (Composition over Inheritance) schon. Dabei deklariert man ein struct und fügt ein Feld ohne Namen (anonymes Feld) hinzu:

// Deklaration der Subklassen Beamter und Arbeiter
/* Die Klasse Lehrer erbt die Eigenschaften der
   Klasse Person durch Komposition
*/
type Beamter struct {
      Person       // Aufnahme einer namenlosen Person
      schule       string
      monatsgehalt int
}
func NewBeamter(p Person, schule string, monatsgehalt int) Beamter {
      beamter := Beamter{p, schule, monatsgehalt}
      return beamter
}
type Arbeiter struct {
      Person     // Aufnahme einer namenlosen Person
      betrieb    string
      monatslohn int
}
func NewArbeiter(p Person, betrieb string, monatslohn int) Arbeiter {
      arbeiter := Arbeiter{p, betrieb, monatslohn}
      return arbeiter
}
/* Das Interface Einkommen deklariert eine
   Funktion JahresGehalt(), die einzelne
   Klassen als Methode unterschiedlich
   implementieren
*/
type Einkommen interface {
      JahresGehalt() int
}
func (b Beamter) JahresGehalt() int {
      return 12 * b.monatsgehalt
}
func (a Arbeiter) JahresGehalt(weihnachtsgeld int) int {
      return 12*a.monatslohn + weihnachtsgeld
}

 
Damit macht man die Felder und Methoden in der struct verfügbar. Diese Technik in Go nennt sich Struct-Embedding. Viele Experten der Objektorientierung betrachten Subclassing als schlechte Praxis und ziehen deshalb die Komposition vor. Über die Deklaration eines interface, das nur eine Methode definiert (diese aber nicht implementiert) erreicht man recht elegant Polymorphismus; da dieses Interface jeden Objekttyp akzeptiert: Objekte, welche die Methode des Interface implementieren – fügen ihm automatisch eine Implementierung hinzu.Das Go-Team bei Goggle stellt für die Programmiersprache Go einen eigenen Language-Server gopls (ausgesprochen: go please) zur Verfügung. Über diesen können Hersteller von IDEs oder Editoren eine Version speziell für Go bereitstellen, um Features wie Auto-Complete, Find-all-References, Go-to-Definition oder Syntax-Highlighting zu implementieren. Deshalb unterstützen alle gängigen IDEs wie Atom, Eclipse oder VS Code/VSCodium eine Entwicklung mit Go.Neben diesen drei bekannten IDEs, stehen zusätzlich zwei spezielle IDEs: GoLand von JetBrains und LiteIDE von visualfc (Bild 6) zur Verfügung. Während es sich bei GoLand um ein kostenpflichtiges Produkt handelt, kann LiteIDE als Open-Source-Software kostenlos bezogen werden. Für die Programmierung mit Go benötigt man in Atom Packages, in Eclipse Plugins und VS Code/VSCodium Extensions. Dabei handelt es sich um Erweiterungen der jeweilige IDE, welche die oben genannten Features zur Bearbeitung von Go-Quellcode unterstützen.
Bei LiteIDEhandelt es sich wie bei GoLand um eine IDE, die vom Hersteller optimal auf das Go-System und die Programmierung mit Go abgestimmt ist(Bild 6) © Simon
Grundsätzlich benötigt man beim Arbeiten mit Atom das go-build-Package, das eine Reihe weiterer Packages zur Installation anfordert. Diese muss man installieren, da ansonsten go-plus die in der linken Tool-Leiste stehenden Befehle nicht ausführt. View > Toogle Console öffnet go-plus mit der Toolleiste; über View > Toogle Command Palette gibt man die verfügbaren Befehle ein. Empfehlenswert für Atom sind zusätzlich ido-gopls (setzt eine Installation des Go-Package gopls voraus) und atom-ide-golang für die Programmierung, go-debug für Debugging mit Delve, go-outline zum Anzeigen der Outline-View und go-oracle für den Einsatz von Go Guru.Für das Arbeiten mit Go in Eclipse steht GoClipse zur Verfügung, dieses Plugin setzt eine installierte Eclipse-IDE voraus. Für VS Code/VCCodium bietet sowohl das Go-Team bei Google als auch die Open-Source-Gemeinde im Marketplace (VS Code) oder in der Open VSX Registry passende Extensions an. Allerdings sollte man auf die Go-Extension von Google (Go for Visual Studio Code) zurückgreifen, da diese laufend gepflegt und sich immer auf dem aktuellen Stand befindet. In allen IDEs kann man für ein Go-System dessen Umgebungsvariablen verändern und an die Notwendigkeiten des Projekts anpassen. Nach dem Speichern dieser Anpassungen stehen sie beim erneuten Öffnen des Projekts wieder zur Verfügung.Im Unterschied Atom, Eclipse oder VS Code/VSCodium implementieren GoLand und LiteIDE für die Programmierung mit Go eine weiterführende Unterstützung direkt in der IDE. So bieten beide IDEs für die Kompilierung eine Auswahl aller von Go unterstützen Plattformen an. Darüber führt man als Programmierer schnell den gewünschten Cross-Compile durch. Beide IDEs ermöglichen einen direkten Zugriff auf die Go-Tools: So stößt man schnell mit go fmt eine Formatierung von Quellcode, über goimports eine Gruppierung der import-Anweisung in Native-Go- als auch Third-Party-Moduls und mit go vet eine Analyse für den Quellcode an. GoLand kennt zusätzlich verschiedene Gültigkeitsbereiche für den GOPATH: Global, Projekt und Modul, die man je nach Bedarf in der IDE konfigurieren kann.

HCL eine DSL-Familie für die Programmierung von DevOps-Tools

HashiCorp verbindet mit HCL die Stärken einer universell einsetzbaren Programmiersprache (General-Purpose-Language/GPL) wie C, Java oder Python mit dem Konzept einer anwendungsorientierten Sprache (Domain-Specific-Language/DSL). Dabei stellt das Anwendungsgebiet (die sogenannte Domain) von HCL die Konfiguration von Cloud-Ressourcen dar. Anstatt mit einer GPL jede Art von Problemen zu lösen, hilft eine DSL bei der bestmöglichen Lösung einer ganz speziellen Aufgabenstellung. Der Entwurf von HCL zielt darauf ab, Wartung und Verwaltung von Cloud- und Multi-Cloud-Umgebungen zu vereinfachen. Dabei orientiert sich HCL an der Einfachheit und Lesbarkeit von JSON.Bei HCL handelt es sich um eine interne beziehungsweise eingebettete DSL, da HCL bei ihrer Implementierung ihrer Sprachelemente auf den wesentlichen Komponenten ihrer Wirtssprache Go aufbaut. Insofern stellt HCL gewissermaßen eine echte Untermenge der generellen Programmiersprache Go da. Mit der Freigabe der verschiedenen HashiCorp-Produkte entstanden verschiedene auf das jeweilige Produkt abgestimmte HCL-Dialekte. Daher kann man HCL als eine ganze Familie von DSLs ansehen. Seitens der Realisierung gibt es bei HashiCorp zwei Entwicklungsstränge, das heißt Versionen von HCL:Einen guten Überblick zu allen derzeit verfügbaren Versionen von HCL erhält man über die Webseite: pkg.go.dev/github.com/hashicorp/hcl?tab=versions. Ein Klick auf den Link Expand all zeigt alle verfügbaren Versionen von HCL 1 an. Der Befehl: go get github.com/hashicorp/hcl installiert HCL 1 mit der für das Go-System aktuell gültigen Modul-Technik. HCL 2 erreicht man über die Webseite: pkg.go.dev/github.com/hashicorp/hcl/v2. Im oberen Fensterausschnitt steht die aktuelle Versionsnummer, ein Klick auf den dahinterstehenden Link zeigt alle verfügbaren Versionen mit dem Datum ihrer Freigabe an. Bei der Auswahl eines Repositorys muss man darauf achten, dass es die Go-Modul-Konventionen unterstützt. Ansonsten müsste man auf eines der älteren Tools für das Management von Abhängigkeiten für die Programmiersprache Go zurückgreifen.Zu den bekannteren Dependency-Management-Tools von Go zählen zum Beispiel dep, glide, godep, gom, govendor, gvt, Rubigo oder Vendetta, die leider aktuell nicht mehr weiterentwickelt oder gepflegt werden.

HCL kennt drei primitive und zwei zusammengesetzte Datentypen

Jeder von den HashiCorp-Produkten eingesetzte HCL-Dialekt stellt die folgenden fünf einfachen Datentypen und Strukturen zur Verfügung: boolean, number, string, Felder (Arrays) und Listen. Mittels dieser primitiven Strukturen und Datentypen schreibt man HCL-Code. Um beispielsweise eine Variable anzulegen, benutzt man die Syntax: variable = wert. Bei variable handelt es sich um den Variablennamen und als Wert greift man auf einen der Datentypen: boolean, string oder number zurück. Der Datentyp boolean entspricht wie in vielen gängigen Programmiersprachen den Werten true oder false. Eine Zeichenkette in HCL definiert man innerhalb zweier doppelter Anführungszeichen: „zeichenkette“.Soll eine Zeichenkette über mehrere Zeilen gehen, so kommt die sogenannte Here Document-Syntax zum Einsatz. Derartige Zeichenkette nennt man auch Multiline-String-Literale. HCL orientiert sich dabei an der HEREDOC-Syntax von PHP, das heißt mehrzeilige Textabschnitte leitet eine <<MARKIERUNG ein, darauf folgt ein Zeilenumbruch und anschließend über mehrere Zeilen der gewünschte Text, den wiederum die Kennung MARKIERUNG am Anfang einer Zeile beendet. Dabei kann MARKIERUNG einen belieben Textstück zum Beispiel MEHRZEILIG entsprechen, dieses muss jedoch durchgängig aus Großbuchstaben bestehen:

&lt;&lt;MEHRZEILIG
Es folgt eine Zeichenkette,
die ueber mehrere Zeilen 
sich verteilt.
MEHRZEILIG
 
 
Der Datentyp number in HCL besteht entweder aus einer ganzen Zahl (int) oder Gleitkommazahl (float). Dabei müssen Zahlen in HCL nicht unbedingt aus Dezimalzahlen bestehen, es sind auch Hexadezimal oder das wissenschaftliche Format erlaubt. Hexadezimalzahlen gibt man mit dem Prefix 0x, Oktalzahlen mit dem Prefix 0 und das wissenschaftliche Format wie häufig üblich mit der e-Notation an. So steht 0x4E für die ganze Zahl 78, 0754 für die Dezimalzahl 492 und 3,517e3 für die Zahl 3517. Eine Kommentarzeile in HCL beginnt mit dem #- oder /-Zeichen, einen mehrzeiligen Kommentar bettet man in die Zeichenfolge /* und */ ein. Die Verfügbarkeit von Kommentaren stellt einen wesentlichen Vorteil von HCL gegenüber dem JSON-Format dar.HCL stellt als zusammengesetzte Datentypen Listen und Arrays (manchmal auch Tuple genannt) zur Verfügung. Ein Array erzeugt man über eine Variablendeklaration zusammen mit dem Einsatz von eckigen Klammern [ ]. Innerhalb der eckigen Klammern kann man jeden der genannten primitiven Datentypen aufführen. Im Unterschied dazu legt man eine Liste (genauer gesagt eine Liste von Objekten) über <type> <variable | objekt-name> und nachfolgender geschwungenen Klammern mit einer Variablendefinition { variable = wert } an. Eine Liste kann weitere Listen enthalten, man spricht von geschachtelten Objekten – manchmal auch von einer Liste von Objekten. Bei einer Liste ist das Vorhandensein eines <type> optional. Beispiele für Listen oder Felder (Arrays) sind:

// Definition eines Array
beispiel_feld=["4711", 47, "drei"]
// Definition einer Variablen über eine Liste
variable "provider" {
    name=“AWS“
}
 
 
Mit Version 2 von HCL hat HashiCorp den Umfang der Konfigurationssprache um die HIL (HashiCorp Interpolation Language) erweitert. Mit HIL zielt HashiCorp darauf ab, Interpolationen für Konfigurationen einzuführen. Genauer gesagt sollte man von String-Interpolationen noch exakter von Schablonen- oder Template-Literalen sprechen. Diese Literale ermöglichen eine einfache Implementierung von Variablen mittels einer sehr einfachen Syntax ${ } und die Einbettung von Ausdrücken. Dabei stellt ${ } ein Syntax-Platzhalter dar, innerhalb dessen man Werte referenziert, die an einer beliebigen anderen Stelle in der Konfiguration auftreten. Die Auswertung des Literals übernimmt HIL und nicht HCL; insofern ist HIL eine eigene Sprache, die lediglich in HCL eingebettet ist.

HCL enthält HIL für die Interpolation von Zeichenketten

Derzeit unterstützt HIL allerdings nur eine einfache Interpolation von Zeichenketten. Berechnungen von Werten über diese Variablen, die Ausführung konditionaler Anweisungen oder die Programmierung von Funktionen ist mit HIL noch nicht möglich. HashiCorp hatte bisher wenig Bedarf, derartige Features zu implementieren. Allerdings lässt es der Hersteller offen, falls eine entsprechende Notwendigkeit vorliegt, diese Fähigkeiten zukünftig dem Sprachumfang von HIL hinzuzufügen. Auch bietet HIL derzeit nicht die Möglichkeit, reguläre Ausdrücke zu formulieren. Bei HIL handelt es sich also nicht um eine reguläre Grammatik, der Aufwand für eine eigene reguläre Sprache empfand HashiCorp als zu groß.Obwohl HIL eine eigene Syntax besitzt, teilt es die mit HCL verfügbaren Sprachkonstrukte – somit kann man innerhalb von HIL beispielsweise Kommentare oder die verfügbaren Datentypen einsetzen oder Funktionen aufrufen. Auch arithmetische Operationen wie Addition, Division, Multiplikation oder Subtraktion kann HIL durchführen. Quellcode in HIL beginnt immer mit dem ${-Zeichen und endet mit einer geschwungenen Klammer, dem }-Zeichen. Alles außerhalb der ${ }-Zeichenfolge behandelt HIL als Literal – somit verknüpft HIL eine in ${ } stehende Zeichenkette mit dem außenstehenden Literal zu einem Ergebnisstring.Beispielsweise verknüpft der HIL-Ausdruck unbekannt ${variable1} bei seiner Ausführung die Zeichenkette “unbekannt“ mit dem Wert von variable1. Enthält variable1 den Wert 4711, ergibt dieser HIL-Ausdruck die Zeichenkette “unbekannt 4711“. Innerhalb eines HIL-Konstrukts ist die Ausführung von Funktionen möglich. Besitzt die variable2 den Wert GROSS und wandelt die Funktion lower eine Zeichenkette in Kleinbuchstaben um, so ergibt zum Beispiel der HIL-Ausdruck ${lower(variable2)} - ${6/2} als Ergebnis die Zeichenkette “gross - 3“. Die über einen HIL-Ausdruck ermittelte Zeichenkette steht nach dessen Ausführung direkt dem HCL-Quellcode zur Verfügung.

Syntax und Arbeitsweise von HCL

Die Syntax von HCL kann man in drei verschiedene Untersprachen einteilen, die alle drei integriert zusammenarbeiten, um als Ergebnis die deklarierte Konfiguration zu erhalten:Im Unterschied zur Structural Language kann man die Syntaxelemente der Expression und der Template Language innerhalb eines HCL-Quellcodes auch eigenständig benutzen. Somit lassen sich Features für eine individuelle HCL-Syntax implementieren, zum Beispiel für eine REPL (Read-Eval-Print-Loop), einen Debugger oder zur Integration spezieller JSON-Formate. Mit der Expression Language stehen imperative Konstrukte in HCL zur Verfügung. Dazu gehören alle Ausdrucke mit Variablen, Funktionen oder For-Expressions für den Aufbau eines Objekts über die Bereitstellung der Bestandteile eines anderen Objekts.Eine Funktion der Expression Language entspricht einer Operation, die über einen Namen verfügt. Funktionen und Variablen besitzen unterschiedliche Namensräume. Allerdings empfiehlt es sich nicht, für eine Variable und eine Funktion denselben Namen zu verwenden. Eine Funktion besteht aus Ausdrücken (Expression), denen man Argumente über die Parameter der Funktion übergeben kann. Das Ergebnis einer Funktion entspricht dem Resultat der Auswertung der ihr zugeordneten Ausdrücke. Eine For-Expression ermöglicht die Durchführung von Iterationen oder die Auswertung von Bedingungen. Eine Iteration leitet das Schlüsselwort for ein, während eine Bedingung mit if beginnt. Beide liefern als Ergebnis eine Expression zurück.

Typen von Konfigurationsblöcken abhängig von der Infrastruktur

Die Structural Language von HCL beschreibt eine Hierarchie mittels Attribute, Blöcke (Blocks) und Bodies. Blöcke selbst deklarieren wiederum eine Hierarchie, die aus einem dem Block zugeordneten Typ und optionalen Labeln besteht. Dabei orientieren sich Typ und optionale Label in HCL am jeweiligen Zielsystem. Möchte man HCL für das Management einer Infrastruktur zum Beispiel in einer Public Cloud beispielsweise Amazon Web Services, Google Cloud Plattform oder Microsoft Azure nutzen, so hat HashiCorp in seinen eigenen DevOps-Tools verschiedene Typen von HCL-Blöcken definiert:Structs (Satzstrukturen) in Golang fassen verschiedenartige Informationen zu einer übergeordneten Einheit zusammen. Satzstrukturen befinden sich häufig in Datenbanken beispielsweise Kunden- oder Adressdaten. So besteht eine Adresse in der Regel aus Ort, Postleitzahl, Straße und Hausnummer. Der Zugriff auf derartige Datenstrukturen erfolgt häufig über ein API, dabei kommen sogenannte Struct-Tags zum Einsatz. Diese Struct-Tags helfen bei der Zuweisung der Daten auf die jeweiligen Felder des zugehörigen Structs. Bei Struct-Tags handelt es sich um Metadaten, die direkt mit dem Feld des Structs verknüpft sind und dieses näher beschreiben. Struct-Tags dienen dazu, anderen Go-Programmen Hilfestellungen für die Bearbeitung eines Structs zu geben. Die Definition eines Struct-Tags erfolgt innerhalb zweier Gravis-Zeichen (Backtick/Backquote): `Struct-Tag-Deklaration`.HashiCorp stellt für HCL ein API in der Programmiersprache Go als Package zur Verfügung. Dieses HCL-API dient dazu in HCL formulierte Konfigurationen, also Beschreibungen für DevOps-Objekte, zu lesen, zu bearbeiten und zu schreiben. Das HCL-Toolkit findet man über https://pkg.go.dev/ als Go-Package, das die Go-Modultechnik unterstützt. Der individuelle, das heißt für das spezifische HCL-Tool entworfene HCL-Code – also die zugehörigen Konfigurationselemente baut auf Go-Structs auf, die mit Tags versehen sind. Das HCL-API verwendet dazu das gohcl-Package: Über dieses sind die Tags wie folgt deklariert: ThingType string `hcl:“thing_type,attr“`. Dabei entspricht thing_typ einem optionalen Namen des zur Konfiguration gehörenden Konstrukts und attr stellt ein optionales Schlüsselwort für die gültigen Typen des Konstrukts dar. Das gohcl-Package kennt neben attr noch weitere Schlüsselwörter: block, label, optional und remain – deren nähere Beschreibung findet man in der gohlc-Dokumentation. Der nachfolgende Quellcode beschreibt zwei Go-Structs mit den Tags einer individuell entworfenen HCL-Konfiguration für spezielle DevOps-Objekte:

type Upstream struct {
    Name    string  `hcl:",label"`
    Type    string  `hcl:"type"`
    Addr    string  `hcl:"addr"`
    Options hcl.Body `hcl:",remain"`
}
type Config struct {
    Listen    string            `hcl:"listen"`
    Upstreams []*Upstream      `hcl:"upstream,block"`
    Rules    map[string]string `hcl:"rules"`
}
 
 
Ein Go-Programm soll selbst kodierte Konfigurations-Objekte auf ihre Korrektheit prüfen. Der Quellcode der Konfigurationen liegt in der oben über Go-Structs formulierten HCL-Syntax vor. Danach gibt es drei verschiedene Konfigurationselemente listen, upstream und rules: Wobei listen eine einfache Zeichenkette darstellt, upstream als Satzstruktur mit zwei optionalen Feldern name, options und den beiden Muss-Feldern type und addr sowie rules aus einer Map mit Zeichenketten als Schlüssel und Werten besteht. Das erste Konfigurations-Objekt in der Datei konf_1.hcl besitzt folgenden Quellcode:

// Konfigurations-Objekt konf_1.hcl
listen = "127.0.0.1:5353"
upstream "mainland" {
  type = "udp"
  addr = "114.114.114.114:53"
}
upstream "oversea" {
  type = "udp"
  addr = "127.0.0.1:53"
}
rules = {
  to_mainland: "mainland",
  default: "oversea"
}
 
 
Das zweite Konfigurations-Objekt befindet sich in der Datei konf_2.hcl und hat folgenden HCL-Quellcode:

// Konfigurations-Objekt konf_2.hcl
upstream "mainland" {
  type = "udp"
  addr = "114.114.114.114:53"
}
upstream „oversea“ {
  type = „udp“
}
rules = {
  to_mainland: "mainland",
  default: "oversea"
}
listen = "127.0.0.1:5353"
 
 
Gemäß der Deklaration der HCL-Elemente über die Go-Struct-Tags muss ein upstream-Element immer die beiden Mussfelder type und addr enthalten, die anderen sind optional. Analysiert man den Quellcode der beiden Konfigurations-Objekte konf_1.hlc und konf_2.hcl, so stellt man fest, dass sich im zweiten Konfigurations-Objekt ein Fehler befindet. Beim upstream-Element mit Bezeichnung oversea fehlt das Mussfeld addr – insofern ist die HCL-Deklaration in der Datei konf_2.hcl nicht vollständig.Ein Programm soll die beiden obenstehenden HCL-Konfigurationen einlesen und anschließend als eigenständige HCL-Elemente ausgeben. Die Ausgabe erfolgt über die Go-Standard-Library mit dem fmt-Modul, das Logging erledigt das log-Modul. Das Hauptprogramm main importiert diese beiden Moduln. Für das Hauptprogramm main erzeugt der Befehl go mod init main die erforderliche Modul-Datei. Anschließend lädt der Befehl go get github.com/hashicorp/hcl/v2@latest die aktuellste Version des HCL-APIs herunter und installiert dieses Package zusammen mit allen abhängigen Beziehungen. Das Lesen erfolgt über die Methode DecodeFile aus dem HCL-Modul hclsimple. Dieses stellt der Import-Befehl import (“github.com/hashicorp/hcl/v2/hclsimple“) dem Hauptprogramm zur Verfügung:

package main
import ( "fmt"
        "log"
          "github.com/hashicorp/hcl/v2"
        "github.com/hashicorp/hcl/v2/hclsimple" )
 
 
Anschließend übernimmt man die Go-Struct-Tag-Definitionen in den Quellcode der main.go-Datei. Die Funktion lesenHCL() liest die in filename übergebene Datei ein, und prüft dabei, ob die HCL-Konfiguration einen Fehler enthält. Das Lesen, genauer gesagt das Parsen, der übergebenen HCL-Datei führt das hclsimple-Objekt mit der Methode DecodeFile durch. Im Fehlerfall erzeugt DecodeFile() ein err-Objekt.Entspricht die eingelesene HCL-Konfiguration der über die Go-Struct-Tags vorgegebenen Syntax – so gibt main die HCL-Datei als Go-Struktur aus. Dabei zeigt main die konkreten Konfigurationselemente für Listen, das Upstream-Array als auch die Rule-Map mit ihren Werten an. Zum Schluss liefert die Funktion lesenHCL() die komplett eingelesene HCL-Konfiguration über die Variable cnfg zurück:

func lesenHCL() (retCnfg Config) {
    // Lesen einer HCL-Deklaration und ihre Ausgabe
    var cnfg Config
    filename := "testdaten/2.hcl"
    fmt.Println()
    err := hclsimple.DecodeFile(filename, nil, &amp;cnfg)
    if err != nil {
        log.Fatalf("HCL-Konfiguration enthält Fehler: %s", err)
    }
    log.Printf("HCL-Datei als Go-Struktur: %#v", cnfg)
    // Die Zeichenkette Listen ausgeben
    fmt.Println()
    log.Printf("Zeichenkette-Listen: %s \n", cnfg.Listen)
    fmt.Println()
    log.Printf("Ausgabe des Upstreams-Array mit seinen Werten\n")
    for index := 0; index &lt; len(cnfg.Upstreams); index++ {
        log.Printf("Array-Nr.: %d \n", index)
        log.Printf("Name: %s \n", cnfg.Upstreams[index].Name)
        log.Printf("Type: %s \n", cnfg.Upstreams[index].Type)
        log.Printf("Addr: %s \n\n", cnfg.Upstreams[index].Addr)
    }
    // Die Rules-Map ausgeben
    log.Printf("Ausgabe der Rule-Map mit ihren Werten\n")
    for key, value := range cnfg.Rules {
        log.Printf("Schlüssel: %s = Wert: %s \n", key, value)
    }
    return cnfg
}

 
Liegt eine syntaktisch korrekte HCL-Datei vor, so führt die Funktion schreibenHCL() eine Ausgabe der eingelesenen Go-Strukturen anhand der mit ihr verbundenen Struct-Tags durch. Dazu benötigt man die Methode NewEmptyFile() des Objekts hclwrite aus dem gleichnamigen Modul. Dies setzt einen passenden Import von github.com/hashicorp/hcl/v2/hclwrite voraus. Nachdem ein leeres HCL-Objekt erzeugt wurde, überführt die Methode EncodeIntoBody() des gohcl-Objekts eine ihr übergebene HCL-Konfiguration in eine lesbare Form und gibt diese aus:

func schreibenHCL(cnfg Config) {
      f := hclwrite.NewEmptyFile()
      gohcl.EncodeIntoBody(&amp;cnfg, f.Body())
      log.Printf("%s", f.Bytes()) }
 
 
Anhand des Go-Befehls go mod tidy überführt man die go.mod-Datei in einen konsistenten Zustand. Anfangs überprüft der mod tidy-Befehl, ob die zugehörigen Module und ihre Abhängigkeiten dem Go-System zugänglich sind. Fehlende Moduln lädt der Befehl herunter und installiert sie. Zusätzlich nimmt go mod tidy alle notwendigen Einträge in die go.mod-Datei vor. Das Lesen, Prüfen und die Ausgabe der HCL-Konfiguration steuert das main-Hauptprogramm:

func main() {
        log.SetFlags(log.Lshortfile)
        fmt.Println()
        log.Printf("Lesen der HCL-Datei in Go-Struktur\n")
        cnfg := lesenHCL()
        fmt.Println()
        log.Printf("Schreiben der HCL-Datei\n")
        schreibenHCL(cnfg)
}
 

Links zum Thema

<b>◼ <a href="https://go.dev/" rel="noopener" target="_blank">Homepage der Programmiersprache Go</a> <br/></b>

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Evolutionäres Prototyping von Business-Apps - Low Code/No Code und KI mit Power Apps
Microsoft baut Power Apps zunehmend mit Features aus, um die Low-Code-/No-Code-Welt mit der KI und der professionellen Programmierung zu verbinden.
19 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige