Go als C#-Entwickler

Go als C#-Entwickler
Page content

Vorwort

Ich habe mir die Programmiersprache Go etwas genauer angeschaut und ein Beispielprojekt erstellt. Als Projekt habe ich mir einen REST-Mockserver überlegt (siehe Projekt auf Github ). Ich habe eine relativ simple Rest-API erstellt und mit HTML/Javascript das Frontend entwickelt. Mit Sqlite werden die Daten in einer lokalen Datenbank gespeichert.

Für das Projekt inkl. Dokumentation habe ich knapp 17h benötigt. In diesem Artikel möchte ich meine persönlichen Highlights aber auch Schwierigkeiten mit Go aufzeigen.

Ich bin kein Go-Profi, daher seht es mir bitte nach, falls wichtige Punkte fehlen :) Am Ende des Artikels habe ich einige Links zu den Frameworks und weiteren Quellen gesammelt.

Syntax

Variablen

Die Syntax ist vergleichbar mit anderen bekannten Programmiersprachen. Es gibt allerdings einen Unterschied bei der Zuweisung bzw. Erstellung von Variablen.

Neue Variablen innerhalb von Funktionen werden mit := eingeleitet. Wenn eine Variable bereits vorhanden ist, ist der Doppelpunkt nicht notwendig.

Funktionen/Methoden

Womit ich mich sehr schwergetan habe, war die Deklaration von “Klassenmethoden”. Ich schreibe das extra in Anführungszeichen, da es in Go keine Klassen gibt. Es gibt nur Structs. Eine Structmethode wird folgendermaßen deklariert:

func (db *DatabaseManager) NameDerMethode(param1 string, param2 int){
  // Auf Variablen in DatabaseManager 'db' kann hier zugegriffen werden
}

Man schreibt vor die Funktion in runden Klammern den Variablennamen und einen Pointer des Structs, dass man verwenden möchte.Wenn eine Methode die Angabe der Klammer (hier: db *DatabaseManager) nicht hat, kann sie allgemein verwendet werden. Die Parameter werden erst mit dem Namen, dann mit dem Typen angegeben.

Eine Funktion kann auch mehrere Rückgabeparameter besitzen. Die Werte aus dem Tupel werden automatisch den Variablen bei der Deklaration zugewiesen.

// Deklaration einer Methode mit 2 Rückgabewerten
func Beispiel() (string, int) {
    return "abc", 22
}

// Aufruf der Funktion. a ist "abc" und b ist 22.
a, b := Beispiel()

Routing/URLs

Mir gefällt die Syntax für Routen sehr gut. Ich finde den Code sehr lesbar und einfach zu warten. Benutzt habe ich dafür gorilla/mux.

router.HandleFunc("/api/mock", httpHandler.CreateMock).Methods("POST")
router.HandleFunc("/create", httpHandler.ShowTemplate).Methods("GET")

Der verwendete Router bietet eine Menge weiterer Möglichkeiten, um Routen zu definieren. In ASP.Net würde man so etwas mit Attributen wie HttpGet oder HttpPost deklarieren.

Was ich mir nicht genauer angeschaut habe, sind eigene Filter bzw. Middlewares. Hier hätte ich meinen Code vereinfachen können, indem eine Middleware prüft, ob es einen Mock mit dem Key gibt. Wenn nicht, dann wird der httpHandler nicht aufgerufen und ein paar If-Abfragen entfallen.

Packages/Namespaces

Packages werden über ein import eingebunden. Dabei gibt es verschiedene Möglichkeiten:

import (
  // Import aus lokalem Ordner
  "./models"

  // Import eines Go-Moduls
  "log"

  // Import eines externen Packages aus github
  "github.com/gorilla/mux"
)

Man kann lokale Packages einbinden, globale Packages und externe Packages aus Github. Lokale Packages kann man sich wie Namespaces vorstellen. “Globale Packages” nenne ich die Packages, welche mit der Installation von Go ausgeliefert werden (beispielsweise log oder net/http). Was ich sehr komfortabel finde, ist das Einbinden von Packages via Github. Die IDE hat das direkt unterstützt und mit einem Klick wurde das Package herunterladen und eingebunden.

So komfortable wünsche ich mir NuGet manchmal :)

Structs statt Klasse

Wenn man Klassen gewohnt ist, wird man hier etwas umdenken müssen. Es gibt nur Structs. Structs sind Werttetypen und es gibt kein null wie man das aus C# kennt. Ebenso muss man sich an Pointer gewöhnen. Im DatabaseManager kann man das sehr schön sehen.

Die Werte aus der SQL-Abfrage werden nicht als Tupel zurückgeliefert. Einen internen Typen wie _ DataRow_ gibt es ebenfalls nicht. Stattdesswen übergeben wir einen Pointer der Variablen, wo der Wert der Zelle gespeichert werden soll. In meiner Abfrage habe ich ein Struct result erstellt, dessen Variablen ich row.Scan(..) übergebe. Dadurch ist das Ergebnis der Abfrage im Struct gespeichert.

func (m *DatabaseManager) GetMock(key string) (result models.JsonMockGet, err error) {
  sql := SELECT id, key, value FROM Mocks WHERE key = ?;`
  row := m.Database.QueryRow(sql, key)
  err = row.Scan(&result.Id, &result.Key, &result.Content)

  // Code verkürzt für bessere Lesbarkeit...

  return result, err
}

Die ersten Aufrufe sind mir etwas schwergefallen. Nach und nach gewöhnt man sich allerdings daran. Ich habe trotzdem einige Stellen mit dem Debugger und einem SQL-Studio geprüft, weil die Felder Id , Key und Content nicht gefüllt wurden und Standardwerte wie "" oder 0 beinhalteten. Irgendwann merkt man, dass hier Pointer benötigt werden :D

HTML-Templates

HTML-Templates haben mich persönlich frustriert. Ich habe eine einfache Syntax erwartet wie beispielsweise bei PHP, ASP.Net Core oder auch Angular.

Es ist möglich in geschweiften Klammern Platzhalter, Bedingungen (if) oder auch Schleifen zu schreiben (hier ein Beispiel ). Grundlegend funktioniert das sehr gut. Es ist mir jedoch nicht auf einfache Weise gelungen, Templates zu verschachteln und einzubinden.Beispielsweise möchte man den Code für Kopfzeile, Navigation oder Fußzeile nur ein Mal schreiben und anschließend über eine Referenz einbinden.

Da das Projekt übersichtlich gehalten ist und nur 3 HTML-Seiten beinhaltet, habe ich den Code 3x kopiert. Wenn jemand eine Webseite mit Go bauen möchte, empfehle ich persönlich ein SPA-Framework wie Vue oder Angular. Wenn man sich mit der Template-Engine auskennt, bietet diese bestimmt eine gute Möglichkeit.

Funktionale Programmierung

Aspekte der funktionalen Programmierung haben mir leider auch gefehlt (oder ich habe diese nicht gefunden). Beim Routing hätte ich den Aufruf an showTemplate mit der Funktion getTemplateForPath gerne etwas vereinfacht.

// Aktuelle Implementierung - Die Methode Show Template prüft
//  intern, welcher Endpunkt (hier 'create') aufgerufen wurde
//  und liefert das Template aus.
router.HandleFunc("/create", httpHandler.ShowTemplate).Methods("GET")


// Wunschsyntax (C#-Like) - Ich reiche die beiden Parameter writer
//  und request durch und übergebe der Funktion einen händischen
//  Parameter mit dem Namen des Templates.
router.HandleFunc("/create", (writer, request) => {
  return httpHandler.ShowTemplate(writer, request, "create")
}).Methods("GET")

Ich hätte mir gewünscht, dass ich den Namen des Templates selbst an die Funktion übergeben kann. HandleFunc erwartet eine Funktion mit 2 Parametern. Lambda Funktionen oder ein Äquivalent gibt es anscheinend (noch?) nicht.

Flags/Argumente

Ich habe Argumente eingebaut, sodass die Anwendung auf einem anderen Port als 8080 ausgeführt werden kann. Hierzu gibt es das Package flag, mit welchem Argument einzelnen Variablen zugewiesen werden können.

flag.IntVar(&port, "port", 8080, "HTTP-Port of the webserver")
flag.Parse()

Ich finde es sehr gut, dass es ein solches Paket im Standard drin ist. Hiermit kann beispielsweise direkt der Parameter -port=1234 genutzt werden, um einen anderen Port für den Server zu nutzen.

Auch hier wird ein Pointer zur Zuweisung des Wertes genutzt. Wenn der Nutzer keinen Wert angibt, kommt der Standardwert zum Zug. Aufpassen muss man beim Aufruf von flag.Parse(). Ohne diesen Aufruf werden die Werte nicht gesetzt!

Nicht verwendete Variablen

Der Go-Kompiler meckert, wenn Variablen deklariert aber nicht genutzt werden. Dieses Verhalten fand ich zwischendurch etwas nervig. Vor allem beim Debuggen und Kennenlernen der Sprache stört es etwas. Ich lege gerne Variablen temporär an, um Inhalte und Rückgabewerte von anderen Klassen und Funktionen anschauen und vergleichen zu können.

Allerdings sorgt das Verhalten dafür, dass keine unnötigen Variablen deklariert werden. Wenn man einen Rückgabewert einer Funktion ignorieren möchte, gibt es den Unterstrich (_) als Platzhalter. Dadurch wird der Rückgabewert einer Funktion ignoriert und keine Variable deklariert.

Kompilieren / Die Anwendung

Der wichtigste Punkt aus meiner Sicht (und der Grund, warum ich Go ausprobieren wollte) ist: Du erhältst eine Anwendung, welche überall lauffähig ist.

In dieser Anwendung ist alles drin: Es müssen keine zusätzlichen Dateien wie DLLs ausgeliefert werden. Interessant dabei ist: Es reicht eine Bibliothek im “Import” zu ergänzen, wodurch die ausführbare Datei automatisch größer wird. Hier warnt aber die IDE (in meinem Fall GoLand), dass nicht genutzt Pakete referenziert sind.

Zudem finde ich es sehr gut, dass Go für jede Plattform kompiliert werden kann. Durch eine Änderung von GOOS kann ich unter Linux die gleiche Anwendung für Linux, Windows und Mac erstellen.

Fazit

Aus meiner Sicht ist Go auf jeden Fall einen Blick wert. Eine Anwendung für alle Betriebssysteme zu schreiben ist sehr praktisch und bietet viel Potenzial, insbesondere für Automatisierung. Da die Anwendung nur aus einer Datei besteht, ist zudem das Deployment sehr einfach möglich.

Für Rest-APIs oder Konsolenanwendungen macht Go einen guten Eindruck. Für komplexe Webseiten halte ich Go ungeeignet (Ausnahme: Single-Page-Application und Go als Rest-backend).


Bildnachweis: Pixabay.com

Linksammlung