4 Go Sprachkritik

Veröffentlicht: 2022-03-11

Go (alias Golang) ist eine der Sprachen, an denen sich die Menschen am meisten interessieren. Seit April 2018 steht sie auf Platz 19 im TIOBE-Index. Immer mehr Menschen wechseln von PHP, Node.js und anderen Sprachen zu Go und verwenden es in der Produktion. Viele coole Software (wie Kubernetes, Docker und Heroku CLI) wird mit Go geschrieben.

Also, was ist Gos Schlüssel zum Erfolg? Es gibt viele Dinge in der Sprache, die sie wirklich cool machen. Aber eines der wichtigsten Dinge, die Go so beliebt gemacht haben, ist seine Einfachheit, wie einer seiner Schöpfer, Rob Pike, betonte.

Einfachheit ist cool: Sie müssen nicht viele Schlüsselwörter lernen. Es macht das Sprachenlernen sehr einfach und schnell. Auf der anderen Seite fehlen Entwicklern jedoch manchmal einige Funktionen, die sie in anderen Sprachen haben, und müssen daher auf lange Sicht Workarounds codieren oder mehr Code schreiben. Leider fehlen Go vom Design her viele Funktionen, und manchmal ist es wirklich nervig.

Golang sollte die Entwicklung beschleunigen, aber in vielen Situationen schreiben Sie mehr Code, als Sie mit anderen Programmiersprachen schreiben würden. Ich werde einige solcher Fälle in meiner Go-Sprachkritik weiter unten beschreiben.

Die 4 Go-Sprachkritiken

1. Fehlende Funktionsüberladung und Standardwerte für Argumente

Ich werde hier ein echtes Codebeispiel posten. Als ich an Golangs Selenium-Bindung arbeitete, musste ich eine Funktion mit drei Parametern schreiben. Zwei davon waren optional. So sieht es nach der Implementierung aus:

 func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }

Ich musste drei verschiedene Funktionen implementieren, weil ich die Funktion nicht einfach überladen oder die Standardwerte übergeben konnte – Go bietet dies nicht per Design. Stellen Sie sich vor, was passieren würde, wenn ich aus Versehen den falschen anrufe? Hier ist ein Beispiel:

Ich würde eine Menge "undefiniert" bekommen

Ich muss zugeben, dass das Überladen von Funktionen manchmal zu unordentlichem Code führen kann. Andererseits müssen Programmierer deswegen mehr Code schreiben.

Wie kann es verbessert werden?

Hier ist das gleiche (na ja, fast das gleiche) Beispiel in JavaScript:

 function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }

Wie Sie sehen können, sieht es viel klarer aus.

Ich mag auch den Ansatz von Elixir. So würde es in Elixir aussehen (ich weiß, dass ich Standardwerte verwenden könnte, wie im obigen Beispiel – ich zeige es nur als Möglichkeit):

 defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")

2. Mangel an Generika

Dies ist wohl die Funktion, nach der Go-Benutzer am meisten fragen.

Stellen Sie sich vor, Sie möchten eine Kartenfunktion schreiben, bei der Sie das Array von Ganzzahlen und die Funktion übergeben, die auf alle ihre Elemente angewendet wird. Klingt einfach, oder?

Machen wir es für ganze Zahlen:

 package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }

Sieht gut aus, oder?

Nun, stellen Sie sich vor, Sie müssten das auch für Strings tun. Sie müssen eine andere Implementierung schreiben, die bis auf die Signatur genau gleich ist. Diese Funktion benötigt einen anderen Namen, da Golang das Überladen von Funktionen nicht unterstützt. Als Ergebnis haben Sie eine Reihe ähnlicher Funktionen mit unterschiedlichen Namen, die in etwa so aussehen:

 func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }

Das widerspricht definitiv dem DRY-Prinzip (Don't Repeat Yourself), das besagt, dass Sie so wenig Copy/Paste-Code wie möglich schreiben und ihn stattdessen in Funktionen verschieben und wiederverwenden müssen.

Ein Mangel an Generika bedeutet Hunderte von Variantenfunktionen

Ein anderer Ansatz wäre, einzelne Implementierungen mit interface{} als Parameter zu verwenden, was jedoch zu einem Laufzeitfehler führen kann, da die Typprüfung zur Laufzeit fehleranfälliger ist. Außerdem wird es langsamer, sodass es keine einfache Möglichkeit gibt, diese Funktionen als eine zu implementieren.

Wie kann es verbessert werden?

Es gibt viele gute Sprachen, die Generika unterstützen. Hier ist zum Beispiel derselbe Code in Rust (ich habe vec anstelle von array verwendet, um es einfacher zu machen):

 fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }

Beachten Sie, dass es eine einzelne Implementierung der map gibt, die für alle benötigten Typen verwendet werden kann, sogar für die benutzerdefinierten.

3. Abhängigkeitsverwaltung

Jeder, der Erfahrung mit Go hat, kann sagen, dass das Abhängigkeitsmanagement wirklich schwierig ist. Mit Go-Tools können Benutzer verschiedene Bibliotheken installieren, indem sie go get <library repo> . Das Problem hier ist die Versionsverwaltung. Wenn der Bibliotheksbetreuer einige rückwärtsinkompatible Änderungen vornimmt und sie auf GitHub hochlädt, erhält jeder, der danach versucht, Ihr Programm zu verwenden, eine Fehlermeldung, da go get nichts anderes tut, als Ihr Repository in einen Bibliotheksordner zu git clone . Auch wenn die Bibliothek nicht installiert ist, wird das Programm deswegen nicht kompiliert.

Sie können es etwas besser machen, indem Sie Dep zum Verwalten von Abhängigkeiten verwenden (https://github.com/golang/dep), aber das Problem hier ist, dass Sie entweder alle Ihre Abhängigkeiten in Ihrem Repository speichern (was nicht gut ist, weil Ihr Repository dies tun wird enthalten nicht nur Ihren Code, sondern Tausende und Abertausende Zeilen Abhängigkeitscode) oder speichern Sie einfach die Paketliste (aber noch einmal, wenn der Betreuer der Abhängigkeit eine rückwärtsinkompatible Änderung vornimmt, wird alles abstürzen).

Wie kann es verbessert werden?

Ich denke, das perfekte Beispiel hier ist Node.js (und JavaScript im Allgemeinen, nehme ich an) und NPM. NPM ist ein Paket-Repository. Es speichert die verschiedenen Versionen von Paketen, wenn Sie also eine bestimmte Version eines Pakets benötigen, kein Problem – Sie können es von dort bekommen. Außerdem ist eines der Dinge in jeder Node.js/JavaScript-Anwendung die Datei package.json . Hier sind alle Abhängigkeiten und ihre Versionen aufgelistet, sodass Sie sie alle mit npm install installieren können (und die Versionen erhalten, die definitiv mit Ihrem Code funktionieren).

Die großartigen Beispiele für die Paketverwaltung sind RubyGems/Bundler (für Ruby-Pakete) und Crates.io/Cargo (für Rust-Bibliotheken).

4. Fehlerbehandlung

Die Fehlerbehandlung in Go ist kinderleicht. In Go können Sie grundsätzlich mehrere Werte von Funktionen zurückgeben, und die Funktion kann einen Fehler zurückgeben. Etwas wie das:

 err, value := someFunction(); if err != nil { // handle it somehow }

Stellen Sie sich nun vor, Sie müssten eine Funktion schreiben, die drei Aktionen ausführt, die einen Fehler zurückgeben. Es wird in etwa so aussehen:

 func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }

Hier gibt es viel wiederholbaren Code, was nicht gut ist. Und bei großen Funktionen kann es noch schlimmer sein! Dazu benötigen Sie wahrscheinlich eine Taste auf Ihrer Tastatur:

humorvolles Bild des Fehlerbehandlungscodes auf einer Tastatur

Wie kann es verbessert werden?

Ich mag den Ansatz von JavaScript. Die Funktion kann einen Fehler auslösen, und Sie können ihn abfangen. Betrachten Sie das Beispiel:

 function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }

Es ist viel klarer und enthält keinen wiederholbaren Code für die Fehlerbehandlung.

Die guten Dinge in Go

Obwohl Go viele Designfehler aufweist, hat es auch einige wirklich coole Funktionen.

1. Goroutinen

Die asynchrone Programmierung wurde in Go wirklich einfach gemacht. Während die Multithreading-Programmierung in anderen Sprachen normalerweise schwierig ist, ist es wirklich einfach, einen neuen Thread zu erzeugen und eine Funktion darin auszuführen, damit der aktuelle Thread nicht blockiert wird:

 func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }

2. Mit Go gebündelte Tools

Während Sie in anderen Programmiersprachen verschiedene Bibliotheken/Tools für verschiedene Aufgaben installieren müssen (z. B. Testen, statische Codeformatierung usw.), gibt es viele coole Tools, die bereits standardmäßig in Go enthalten sind, wie zum Beispiel:

  • gofmt - Ein Tool zur statischen Codeanalyse. Im Vergleich zu JavaScript, wo Sie eine zusätzliche Abhängigkeit wie eslint oder jshint installieren müssen, ist es hier standardmäßig enthalten. Und das Programm wird nicht einmal kompiliert, wenn Sie keinen Code im Go-Stil schreiben (keine deklarierten Variablen verwenden, nicht verwendete Pakete importieren usw.).
  • go test - Ein Testframework. Auch hier müssen Sie im Vergleich zu JavaScript zusätzliche Abhängigkeiten zum Testen installieren (Jest, Mocha, AVA usw.). Hier ist es standardmäßig enthalten. Und es erlaubt Ihnen, standardmäßig viele coole Dinge zu tun, wie z. B. Benchmarking, Konvertieren von Code in der Dokumentation in Tests usw.
  • godoc - Ein Dokumentationstool. Es ist schön, dass es in den Standardwerkzeugen enthalten ist.
  • Der Compiler selbst. Es ist unglaublich schnell im Vergleich zu anderen kompilierten Sprachen!

3. Aufschieben

Ich denke, das ist eines der nettesten Features in der Sprache. Stellen Sie sich vor, Sie müssten eine Funktion schreiben, die drei Dateien öffnet. Und wenn etwas fehlschlägt, müssen Sie vorhandene geöffnete Dateien schließen. Wenn es viele solcher Konstruktionen gibt, wird es wie ein Durcheinander aussehen. Betrachten Sie dieses Pseudo-Code-Beispiel:

 function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }

Sieht kompliziert aus. Hier kommt Go's defer ins Spiel:

 package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }

Wie Sie sehen, werden andere Dateien automatisch geschlossen, wenn wir beim Öffnen von Datei Nummer drei einen Fehler erhalten, da die defer -Anweisungen vor der Rückgabe in umgekehrter Reihenfolge ausgeführt werden. Außerdem ist es schön, das Öffnen und Schließen von Dateien an derselben Stelle statt an verschiedenen Teilen einer Funktion zu haben.

Fazit

Ich habe nicht alle guten und schlechten Dinge in Go erwähnt, nur die, die ich für die besten und die schlechtesten Dinge halte.

Go ist wirklich eine der interessanten Programmiersprachen, die derzeit verwendet werden, und sie hat wirklich Potenzial. Es bietet uns wirklich coole Tools und Funktionen. Allerdings gibt es dort viele Dinge, die verbessert werden können.

Wenn wir als Go-Entwickler diese Änderungen umsetzen, wird dies unserer Community sehr zugute kommen, da das Programmieren mit Go viel angenehmer wird.

Wenn Sie in der Zwischenzeit versuchen, Ihre Tests mit Go zu verbessern, versuchen Sie Testing Your Go App: Get Started the Right Way von Toptaler Gabriel Aszalos.

Siehe auch : Gut strukturierte Logik: Ein Golang-OOP-Tutorial