Warum Go für unintelligente Programmierer schlecht ist

Der Artikel wurde als Antwort auf einen zuvor veröffentlichten Artikel verfasst antipodischer Artikel.

Warum Go für unintelligente Programmierer schlecht ist

In den letzten mehr als zwei Jahren habe ich Go verwendet, um einen speziellen RADIUS-Server mit einem entwickelten Abrechnungssystem zu implementieren. Unterwegs lerne ich die Feinheiten der Sprache selbst kennen. Die Programme selbst sind sehr einfach und nicht der Zweck dieses Artikels, aber die Erfahrung bei der Verwendung von Go selbst verdient ein paar Worte zu ihrer Verteidigung. Go wird immer mehr zu einer Mainstream-Sprache für seriösen, skalierbaren Code. Die Sprache wurde von Google erstellt und wird dort aktiv verwendet. Unterm Strich bin ich ehrlich gesagt der Meinung, dass das Design der Go-Sprache für unintelligente Programmierer schlecht ist.

Für schwache Programmierer konzipiert?

Die Schwachen sprechen von Problemen. Das starke Gerede über Ideen und Träume...

Go ist sehr leicht zu erlernen, so einfach, dass Sie den Code praktisch ohne Schulung lesen können. Dieses Merkmal der Sprache wird in vielen globalen Unternehmen verwendet, wenn der Code zusammen mit nicht zum Kerngeschäft gehörenden Spezialisten (Managern, Kunden usw.) gelesen wird. Dies ist sehr praktisch für Methoden wie Design Driven Development.
Selbst unerfahrene Programmierer beginnen nach ein oder zwei Wochen, recht guten Code zu produzieren. Das Buch, aus dem ich gelernt habe, ist „Go Programming“ (von Mark Summerfield). Das Buch ist sehr gut, es berührt viele Nuancen der Sprache. Nach unnötig komplizierten Sprachen wie Java, PHP ist der Mangel an Magie erfrischend. Doch früher oder später kommen viele eingeschränkte Programmierer auf die Idee, alte Methoden in einem neuen Bereich einzusetzen. Ist das wirklich notwendig?

Rob Pike (der Hauptideologe der Sprache) hat die Go-Sprache als Industriesprache geschaffen, die leicht zu verstehen und effektiv zu verwenden ist. Die Sprache ist auf maximale Produktivität in großen Teams ausgelegt und daran besteht kein Zweifel. Viele unerfahrene Programmierer beschweren sich darüber, dass ihnen viele Funktionen fehlen. Dieser Wunsch nach Einfachheit war eine bewusste Entscheidung der Designer der Sprache, und um vollständig zu verstehen, warum er notwendig war, müssen wir die Motivation der Entwickler verstehen und wissen, was sie mit Go erreichen wollten.

Warum wurde es so einfach gemacht? Hier sind ein paar Zitate von Rob Pike:

Der entscheidende Punkt hierbei ist, dass unsere Programmierer keine Forscher sind. Sie sind in der Regel recht jung, kommen nach dem Studium zu uns, vielleicht haben sie Java, C/C++ oder Python studiert. Sie können keine großartige Sprache verstehen, aber gleichzeitig möchten wir, dass sie gute Software erstellen. Deshalb sollte die Sprache leicht zu verstehen und zu erlernen sein.

Er sollte bekannt sein, grob gesagt ähnlich wie C. Programmierer, die bei Google arbeiten, beginnen ihre Karriere früh und sind meist mit prozeduralen Sprachen, insbesondere der C-Familie, vertraut. Die Anforderung an eine schnelle Produktivität in einer neuen Programmiersprache bedeutet, dass die Sprache nicht zu radikal sein sollte.

Weise Worte, nicht wahr?

Artefakte der Einfachheit

Einfachheit ist eine notwendige Voraussetzung für Schönheit. Lew Tolstoi.

Es einfach zu halten ist eines der wichtigsten Ziele bei jedem Design. Wie Sie wissen, ist ein perfektes Projekt kein Projekt, bei dem es nichts hinzuzufügen gibt, sondern eines, bei dem es nichts zu entfernen gibt. Viele Menschen glauben, dass zur Lösung (oder auch nur zum Ausdruck) komplexer Probleme ein komplexes Werkzeug erforderlich ist. Dies ist jedoch nicht der Fall. Nehmen wir als Beispiel die Sprache PERL. Sprachideologen glaubten, dass ein Programmierer mindestens drei verschiedene Möglichkeiten haben sollte, ein Problem zu lösen. Die Ideologen der Go-Sprache gingen einen anderen Weg; sie kamen zu dem Schluss, dass ein Weg, aber ein wirklich guter, ausreichte, um das Ziel zu erreichen. Dieser Ansatz hat eine ernsthafte Grundlage: Der einzige Weg ist leichter zu lernen und schwerer zu vergessen.

Viele Migranten beklagen, dass die Sprache keine eleganten Abstraktionen enthält. Ja, das stimmt, aber das ist einer der Hauptvorteile der Sprache. Die Sprache enthält ein Minimum an Magie – es sind also keine tiefen Kenntnisse erforderlich, um das Programm zu lesen. Was die Ausführlichkeit des Codes betrifft, ist dies überhaupt kein Problem. Ein gut geschriebenes Golang-Programm liest vertikal und hat wenig oder keine Struktur. Darüber hinaus ist die Geschwindigkeit beim Lesen eines Programms mindestens eine Größenordnung höher als die Geschwindigkeit beim Schreiben. Wenn man bedenkt, dass der gesamte Code eine einheitliche Formatierung aufweist (durch den integrierten Befehl gofmt), ist das Lesen einiger zusätzlicher Zeilen überhaupt kein Problem.

Nicht sehr ausdrucksstark

Die Kunst duldet keine Einschränkung ihrer Freiheit. Genauigkeit liegt nicht in seiner Verantwortung.

Aufgrund des Wunsches nach Einfachheit fehlen Go Konstrukte, die in anderen Sprachen von Menschen, die daran gewöhnt sind, als etwas Natürliches wahrgenommen werden. Im ersten Moment mag es etwas umständlich sein, aber dann merkt man, dass das Programm viel einfacher und eindeutiger zu lesen ist.

Ein Konsolendienstprogramm, das stdin oder eine Datei aus Befehlszeilenargumenten liest, würde beispielsweise so aussehen:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()

    scanner := newScanner(flag.Args())

    var text string
    for scanner.Scan() {
        text += scanner.Text()
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

func newScanner(flags []string) *bufio.Scanner {
    if len(flags) == 0 {
        return bufio.NewScanner(os.Stdin)
    }

    file, err := os.Open(flags[0])

    if err != nil {
        log.Fatal(err)
    }

    return bufio.NewScanner(file)
}

Die Lösung des gleichen Problems in D sieht zwar etwas kürzer aus, ist aber nicht einfacher zu lesen

import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

Hölle des Kopierens

Der Mensch trägt die Hölle in sich. Martin Luther.

Anfänger beschweren sich ständig über den Mangel an Generika bei Go. Um dieses Problem zu lösen, verwenden die meisten von ihnen das direkte Kopieren von Code. Wenn es sich zum Beispiel um eine Funktion zum Summieren einer Liste von ganzen Zahlen handelt, glauben solche Möchtegern-Profis, dass die Funktionalität nicht anders als durch einfaches Kopieren und Einfügen für jeden Datentyp implementiert werden kann.

package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

Die Sprache verfügt über ausreichende Mittel, solche Konstruktionen umzusetzen. Beispielsweise wäre generische Programmierung in Ordnung.

package main

import "fmt"

func Eval32(list []int32, fn func(a, b int32)int32) int32 {
    var res int32
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int32Add(a, b int32) int32 {
    return a + b
}

func int32Sub(a, b int32) int32 {
    return a + b
}

func Eval64(list []int64, fn func(a, b int64)int64) int64 {
    var res int64
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int64Add(a, b int64) int64 {
    return a + b
}

func int64Sub(a, b int64) int64 {
    return a - b
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(Eval32(list32, int32Add))
    fmt.Println(Eval64(list64, int64Add))
    fmt.Println(Eval64(list64, int64Sub))
}

Und obwohl sich herausstellte, dass unser Code etwas länger war als im vorherigen Fall, wurde er verallgemeinert. Daher wird es für uns nicht schwierig sein, alle arithmetischen Operationen umzusetzen.

Viele werden sagen, dass ein Programm in D deutlich kürzer aussieht, und sie werden Recht haben.

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

Allerdings ist es nur kürzer, aber nicht korrekter, da die D-Implementierung das Problem der Fehlerbehandlung völlig außer Acht lässt.

Im wirklichen Leben wird die Lücke mit zunehmender Komplexität der Logik schnell kleiner. Die Lücke schließt sich noch schneller, wenn Sie eine Aktion ausführen müssen, die mit Standardsprachoperatoren nicht ausgeführt werden kann.

In Bezug auf Wartbarkeit, Erweiterbarkeit und Lesbarkeit gewinnt meiner Meinung nach die Go-Sprache, obwohl sie an Ausführlichkeit verliert.

Die verallgemeinerte Programmierung bringt uns in manchen Fällen unbestreitbare Vorteile. Dies wird durch das Sortierpaket deutlich. Um also eine beliebige Liste zu sortieren, müssen wir lediglich die Schnittstelle sort.Interface implementieren.

import "sort"

type Names []string

func (ns Names) Len() int {
    return len(ns)
}

func (ns Names) Less(i, j int) bool {
    return ns[i] < ns[j]
}

func (ns Names) Swap(i, j int) {
    ns[i], ns[j] = ns[j], ns[i]
}

func main() {
    names := Names{"London", "Berlin", "Rim"}
    sort.Sort(names)
}

Wenn Sie ein beliebiges Open-Source-Projekt nehmen und den Befehl grep „interface{}“ -R ausführen, werden Sie feststellen, wie oft verwirrende Schnittstellen verwendet werden. Engstirnige Genossen werden sofort sagen, dass dies alles auf den Mangel an Generika zurückzuführen ist. Dies ist jedoch nicht immer der Fall. Nehmen wir DELPHI als Beispiel. Trotz des Vorhandenseins derselben Generika enthält es einen speziellen VARIANT-Typ für Operationen mit beliebigen Datentypen. Die Go-Sprache macht dasselbe.

Von einer Waffe auf Spatzen

Und die Zwangsjacke muss zur Größe des Wahnsinns passen. Stanislav Lec.

Viele Extremfans behaupten möglicherweise, dass Go über einen weiteren Mechanismus zum Erstellen von Generika verfügt – die Reflexion. Und sie werden Recht haben ... aber nur in seltenen Fällen.

Rob Pike warnt uns:

Dies ist ein leistungsstarkes Tool, das mit Vorsicht verwendet werden sollte. Es sollte vermieden werden, es sei denn, es ist unbedingt erforderlich.

Wikipedia sagt uns Folgendes:

Reflexion bezieht sich auf den Prozess, bei dem ein Programm seine eigene Struktur und sein eigenes Verhalten während der Ausführung überwachen und ändern kann. Das der Reflexion zugrunde liegende Programmierparadigma wird als reflektierende Programmierung bezeichnet. Dies ist eine Art Metaprogrammierung.

Allerdings muss man bekanntlich für alles bezahlen. In diesem Fall ist es:

  • Schwierigkeiten beim Schreiben von Programmen
  • Geschwindigkeit der Programmausführung

Daher muss die Reflexion wie eine großkalibrige Waffe mit Vorsicht eingesetzt werden. Der gedankenlose Einsatz von Reflexion führt zu unlesbaren Programmen, ständigen Fehlern und geringer Geschwindigkeit. Genau das Richtige für einen versnobten Programmierer, um seinen Code vor anderen, pragmatischeren und bescheideneren Kollegen zur Schau stellen zu können.

Kulturgepäck von Xi? Nein, aus mehreren Sprachen!

Den Erben verbleiben neben dem Vermögen auch Schulden.

Obwohl viele glauben, dass die Sprache vollständig auf dem C-Erbe basiert, ist dies nicht der Fall. Die Sprache beinhaltet viele Aspekte der besten Programmiersprachen.

Syntax

Zunächst einmal basiert die Syntax grammatikalischer Strukturen auf der Syntax der C-Sprache. Allerdings hatte auch die DELPHI-Sprache einen erheblichen Einfluss. Wir sehen also, dass die überflüssigen Klammern, die die Lesbarkeit des Programms stark beeinträchtigen, vollständig entfernt wurden. Die Sprache enthält auch den „:=“-Operator, der der DELPHI-Sprache eigen ist. Das Konzept der Pakete ist Sprachen wie ADA entlehnt. Die Deklaration nicht verwendeter Entitäten ist der PROLOG-Sprache entlehnt.

Semantik

Die Pakete basierten auf der Semantik der DELPHI-Sprache. Jedes Paket kapselt Daten und Code und enthält private und öffentliche Einheiten. Dadurch können Sie die Paketschnittstelle auf ein Minimum reduzieren.

Die Implementierungsoperation durch Delegationsmethode wurde aus der DELPHI-Sprache entlehnt.

Zusammenstellung

Nicht umsonst gibt es einen Witz: Go sei während der Kompilierung eines C-Programms entstanden. Eine der Stärken der Sprache ist ihre ultraschnelle Kompilierung. Die Idee wurde der DELPHI-Sprache entlehnt. Jedes Go-Paket entspricht einem DELPHI-Modul. Diese Pakete werden nur dann neu kompiliert, wenn es wirklich notwendig ist. Daher müssen Sie nach der nächsten Bearbeitung nicht das gesamte Programm kompilieren, sondern nur die geänderten Pakete und die Pakete, die von diesen geänderten Paketen abhängen, neu kompilieren (und selbst dann nur, wenn sich die Paketschnittstellen geändert haben).

Konstrukte auf hohem Niveau

Die Sprache enthält viele verschiedene High-Level-Konstrukte, die in keiner Weise mit Low-Level-Sprachen wie C verwandt sind.

  • Zeichenketten
  • Hash-Tabellen
  • Scheiben
  • Duck Typing ist Sprachen wie RUBY entlehnt (die viele leider nicht verstehen oder nicht in vollem Umfang nutzen).

Speicherverwaltung

Die Speicherverwaltung verdient im Allgemeinen einen separaten Artikel. Wenn in Sprachen wie C++ die Kontrolle vollständig dem Entwickler überlassen bleibt, wurde in späteren Sprachen wie DELPHI ein Referenzzählmodell verwendet. Bei diesem Ansatz waren zyklische Referenzen nicht zulässig, da verwaiste Cluster gebildet wurden. Go verfügt dann über eine integrierte Erkennung solcher Cluster (wie C#). Darüber hinaus ist der Garbage Collector effizienter als die meisten derzeit bekannten Implementierungen und kann bereits für viele Echtzeitaufgaben eingesetzt werden. Die Sprache selbst erkennt Situationen, in denen ein Wert zum Speichern einer Variablen auf dem Stapel zugewiesen werden kann. Dies reduziert die Belastung des Speichermanagers und erhöht die Geschwindigkeit des Programms.

Parallelität und Parallelität

Die Parallelität und Wettbewerbsfähigkeit der Sprache ist nicht zu loben. Keine Low-Level-Sprache kann auch nur annähernd mit Go konkurrieren. Fairerweise muss man erwähnen, dass das Modell nicht von den Autoren der Sprache erfunden wurde, sondern einfach der guten alten ADA-Sprache entlehnt wurde. Die Sprache ist in der Lage, Millionen paralleler Verbindungen unter Verwendung aller CPUs zu verarbeiten und weist gleichzeitig um eine Größenordnung weniger komplexe Probleme mit Deadlocks und Race-Bedingungen auf, die für Multithread-Code typisch sind.

Zusätzliche Vorteile

Wenn es profitabel ist, werden alle selbstlos.

Die Sprache bietet uns zweifellos auch eine Reihe von Vorteilen:

  • Eine einzige ausführbare Datei nach der Erstellung des Projekts vereinfacht die Bereitstellung von Anwendungen erheblich.
  • Durch statische Typisierung und Typinferenz kann die Anzahl der Fehler in Ihrem Code erheblich reduziert werden, auch ohne Tests zu schreiben. Ich kenne einige Programmierer, die ganz auf das Schreiben von Tests verzichten und die Qualität ihres Codes dadurch nicht wesentlich leidet.
  • Sehr einfache übergreifende Kompilierung und hervorragende Portabilität der Standardbibliothek, was die Entwicklung plattformübergreifender Anwendungen erheblich vereinfacht.
  • Reguläre RE2-Ausdrücke sind threadsicher und haben vorhersehbare Ausführungszeiten.
  • Eine leistungsstarke Standardbibliothek, die es den meisten Projekten ermöglicht, auf Frameworks von Drittanbietern zu verzichten.
  • Die Sprache ist leistungsstark genug, um sich auf das Problem zu konzentrieren und nicht darauf, wie es gelöst werden soll, und dennoch niedrig genug, um das Problem effizient lösen zu können.
  • Das Go-Ökosystem enthält bereits entwickelte Tools für alle Gelegenheiten: Tests, Dokumentation, Paketverwaltung, leistungsstarke Linters, Codegenerierung, Race-Conditions-Detektor usw.
  • Go Version 1.11 führte ein integriertes semantisches Abhängigkeitsmanagement ein, das auf dem beliebten VCS-Hosting aufbaut. Alle Tools, aus denen das Go-Ökosystem besteht, nutzen diese Dienste, um auf einen Schlag Code von ihnen herunterzuladen, zu erstellen und zu installieren. Und das ist großartig. Mit der Einführung der Version 1.11 wurde auch das Problem mit der Paketversionierung vollständig behoben.
  • Da die Kernidee der Sprache darin besteht, Magie zu reduzieren, bietet die Sprache Entwicklern einen Anreiz, die Fehlerbehandlung explizit durchzuführen. Und das ist richtig, denn sonst wird die Fehlerbehandlung einfach völlig vergessen. Eine andere Sache ist, dass die meisten Entwickler die Fehlerbehandlung bewusst ignorieren und es vorziehen, den Fehler einfach nach oben weiterzuleiten, anstatt sie zu verarbeiten.
  • Die Sprache implementiert nicht die klassische OOP-Methodik, da es in ihrer reinen Form keine Virtualität in Go gibt. Bei der Verwendung von Schnittstellen stellt dies jedoch kein Problem dar. Das Fehlen von OOP verringert die Einstiegshürde für Anfänger erheblich.

Einfachheit zum Nutzen der Gemeinschaft

Es ist leicht zu komplizieren, aber schwer zu vereinfachen.

Go wurde so konzipiert, dass es einfach ist, und dieses Ziel wird erreicht. Es wurde für intelligente Programmierer geschrieben, die die Vorteile der Teamarbeit verstehen und die endlose Variabilität der Sprachen auf Unternehmensebene satt haben. Da es in seinem Arsenal über einen relativ kleinen Satz syntaktischer Strukturen verfügt, unterliegt es im Laufe der Zeit praktisch keinen Änderungen, sodass Entwickler viel Zeit für die Entwicklung und nicht für das endlose Studium von Sprachinnovationen haben.

Darüber hinaus profitieren Unternehmen von zahlreichen Vorteilen: Eine niedrige Einstiegshürde ermöglicht es ihnen, schnell einen Spezialisten zu finden, und die Unveränderlichkeit der Sprache ermöglicht es ihnen, auch nach 10 Jahren den gleichen Code zu verwenden.

Abschluss

Die große Gehirngröße hat noch nie einen Elefanten zum Nobelpreisträger gemacht.

Für Programmierer, deren persönliches Ego Vorrang vor Teamgeist hat, sowie für Theoretiker, die akademische Herausforderungen und endlose „Selbstverbesserung“ lieben, ist die Sprache wirklich schlecht, da es sich um eine handwerkliche Allzwecksprache handelt, die es einem nicht erlaubt, sie zu verstehen ästhetische Freude am Ergebnis Ihrer Arbeit und zeigen Sie sich vor Kollegen professionell (vorausgesetzt, wir messen Intelligenz an diesen Kriterien und nicht am IQ). Wie alles im Leben ist es eine Frage der persönlichen Prioritäten. Wie alle lohnenswerten Innovationen hat die Sprache bereits einen langen Weg von der allgemeinen Ablehnung zur Massenakzeptanz zurückgelegt. Die Sprache ist in ihrer Einfachheit genial, und wie Sie wissen, ist alles Geniale einfach!

Source: habr.com

Kommentar hinzufügen