Waarom Go Design slecht is voor slimme programmeurs

De afgelopen maanden heb ik Go gebruikt voor implementaties. Proof of Concept (ca.: code om de functionaliteit van een idee te testen) in zijn vrije tijd, deels om de programmeertaal zelf te bestuderen. De programma's zelf zijn heel eenvoudig en vormen niet het doel van dit artikel, maar de ervaring met het gebruik van Go zelf verdient er een paar woorden over. Go belooft te worden (ca.: artikel geschreven in 2015) een populaire taal voor serieuze schaalbare code. De taal is gemaakt door Google, waar deze actief wordt gebruikt. Kortom, ik denk eerlijk gezegd dat het ontwerp van de Go-taal slecht is voor slimme programmeurs.

Ontworpen voor zwakke programmeurs?

Go is heel makkelijk te leren, zo makkelijk dat de introductie mij één avond kostte, waarna ik al productief kon coderen. Het boek dat ik vroeger leerde heet Go Een inleiding tot programmeren in Go (vertaling), het is online beschikbaar. Het boek is, net als de Go-broncode zelf, gemakkelijk te lezen, bevat goede codevoorbeelden en bevat ongeveer 150 pagina's die in één keer kunnen worden gelezen. Deze eenvoud is in eerste instantie verfrissend, vooral in een programmeerwereld vol ingewikkelde technologie. Maar uiteindelijk komt vroeg of laat de gedachte op: “Is dit echt zo?”

Google beweert dat de eenvoud van Go het verkoopargument is en dat de taal is ontworpen voor maximale productiviteit in grote teams, maar ik betwijfel dat. Er zijn functies die ontbreken of te gedetailleerd zijn. En dat allemaal vanwege een gebrek aan vertrouwen in ontwikkelaars, in de veronderstelling dat ze niets goed kunnen doen. Dit verlangen naar eenvoud was een bewuste beslissing van de ontwerpers van de taal, en om volledig te begrijpen waarom dit nodig was, moeten we de motivatie van de ontwikkelaars begrijpen en wat ze probeerden te bereiken met Go.

Dus waarom werd het zo eenvoudig gemaakt? Hier zijn een paar citaten Rob Pike (ca.: een van de mede-makers van de Go-taal):

Het belangrijkste punt hier is dat onze programmeurs (ca.: Googlers) zijn geen onderzoekers. Ze zijn in de regel vrij jong en komen na hun studie naar ons toe, misschien hebben ze Java gestudeerd, of C/C++, of Python. Ze begrijpen geen geweldige taal, maar tegelijkertijd willen we dat ze goede software maken. Daarom moet hun taal voor hen gemakkelijk te begrijpen en te leren zijn.
 
Hij zou bekend moeten zijn, grofweg vergelijkbaar met C. Programmeurs die bij Google werken, beginnen hun carrière vroeg en zijn meestal bekend met proceduretalen, in het bijzonder de C-familie. De eis voor snelle productiviteit in een nieuwe programmeertaal betekent dat de taal niet te radicaal mag zijn.

Wat? Dus Rob Pike zegt eigenlijk dat de ontwikkelaars bij Google niet zo goed zijn, daarom hebben ze een taal voor idioten gemaakt (ca.: gedumpt) zodat ze iets kunnen doen. Wat voor arrogante blik op je eigen collega's? Ik heb altijd geloofd dat de ontwikkelaars van Google zorgvuldig zijn uitgekozen uit de slimste en beste ter wereld. Ze kunnen toch wel iets moeilijkers aan?

Artefacten van buitensporige eenvoud

Eenvoudig zijn is een waardevol doel bij elk ontwerp, en iets eenvoudigs proberen te maken is moeilijk. Bij het oplossen (of zelfs uitdrukken) van complexe problemen is echter soms een complex hulpmiddel nodig. Complexiteit en ingewikkeldheid zijn niet de beste eigenschappen van een programmeertaal, maar er is een middenweg waarin de taal elegante abstracties kan creëren die gemakkelijk te begrijpen en te gebruiken zijn.

Niet erg expressief

Vanwege zijn streven naar eenvoud mist Go constructies die in andere talen als natuurlijk worden ervaren. Dit lijkt in eerste instantie misschien een goed idee, maar in de praktijk resulteert het in uitgebreide code. De reden hiervoor zou duidelijk moeten zijn: het moet voor ontwikkelaars gemakkelijk zijn om de code van anderen te lezen, maar in feite zijn deze vereenvoudigingen alleen maar schadelijk voor de leesbaarheid. Er zijn geen afkortingen in Go: veel of niets.

Een consolehulpprogramma dat stdin of een bestand van opdrachtregelargumenten leest, zou er bijvoorbeeld als volgt uitzien:

package main

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

func main() {

    flag.Parse()
    flags := flag.Args()

    var text string
    var scanner *bufio.Scanner
    var err error

    if len(flags) > 0 {

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

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

        scanner = bufio.NewScanner(file)

    } else {
        scanner = bufio.NewScanner(os.Stdin)
    }

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

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

    fmt.Println(text)
}

Hoewel deze code ook zo algemeen mogelijk probeert te zijn, staat Go's geforceerde breedsprakigheid in de weg, en als gevolg daarvan resulteert het oplossen van een eenvoudig probleem in een grote hoeveelheid code.

Hier vindt u bijvoorbeeld een oplossing voor hetzelfde probleem D:

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);
    }
}

En wie is nu beter leesbaar? Ik geef mijn stem aan D. Zijn code is veel leesbaarder omdat hij de acties duidelijker beschrijft. D gebruikt veel complexere concepten (ca.: alternatieve functieaanroep и Sjablonen) dan in het Go-voorbeeld, maar er is niets ingewikkelds aan het begrijpen ervan.

Een hel van kopiëren

Een populaire suggestie om Go te verbeteren is algemeenheid. Dit helpt in ieder geval onnodig kopiëren van code ter ondersteuning van alle gegevenstypen te voorkomen. Een functie voor het optellen van een lijst met gehele getallen kan bijvoorbeeld op geen andere manier worden geïmplementeerd dan door de basisfunctie ervan voor elk type geheel getal te kopiëren en te plakken; er is geen andere manier:

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 int16Sum(list []int16) (uint64) {
    var result int16 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

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

func main() {

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

    fmt.Println(int8Sum(list8))
    fmt.Println(int16Sum(list16))
    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

En dit voorbeeld werkt niet eens voor ondertekende typen. Deze aanpak schendt volledig het principe om jezelf niet te herhalen (DRY), een van de meest bekende en voor de hand liggende principes, waarbij wordt genegeerd dat dit de bron is van veel fouten. Waarom doet Go dit? Dit is een vreselijk aspect van taal.

Hetzelfde voorbeeld op D:

import std.stdio;
import std.algorithm;

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

Simpel, elegant en straight to the point. De hier gebruikte functie is reduce voor sjabloontype en predikaat. Ja, dit is wederom ingewikkelder dan de Go-versie, maar niet zo moeilijk voor slimme programmeurs om te begrijpen. Welk voorbeeld is gemakkelijker te onderhouden en gemakkelijker te lezen?

Eenvoudig type systeembypass

Ik kan me voorstellen dat Go-programmeurs die dit lezen, schuim op hun mond zullen hebben en zullen schreeuwen: "Je doet het verkeerd!" Welnu, er is een andere manier om een ​​generieke functie en typen te maken, maar deze verbreekt het typesysteem volledig!

Kijk eens naar dit voorbeeld van een stomme taaloplossing om het probleem te omzeilen:

package main

import "fmt"
import "reflect"

func Reduce(in interface{}, memo interface{}, fn func(interface{}, interface{}) interface{}) interface{} {
    val := reflect.ValueOf(in)

    for i := 0; i < val.Len(); i++ {
        memo = fn(val.Index(i).Interface(), memo)
    }

    return memo
}

func main() {

    list := []int{1, 2, 3, 4, 5}

    result := Reduce(list, 0, func(val interface{}, memo interface{}) interface{} {
        return memo.(int) + val.(int)
    })

    fmt.Println(result)
}

Deze implementatie Reduce is ontleend aan het artikel Idiomatische generieke geneesmiddelen in Go (ca.: Ik kon de vertaling niet vinden, ik zal blij zijn als je hierbij helpt). Als het idiomatisch is, zou ik niet graag een niet-idiomatisch voorbeeld zien. Gebruik interface{} - een farce, en in de taal is het alleen nodig om het typen te omzeilen. Dit is een lege interface en alle typen implementeren deze, waardoor iedereen volledige vrijheid heeft. Deze programmeerstijl is vreselijk lelijk, en dat is nog niet alles. Dergelijke acrobatische prestaties vereisen het gebruik van runtime-reflectie. Zelfs Rob Pike houdt niet van mensen die hier misbruik van maken, zoals hij in een van zijn rapporten vermeldde.

Dit is een krachtig hulpmiddel dat met voorzichtigheid moet worden gebruikt. Het moet worden vermeden, tenzij strikt noodzakelijk.

Ik zou D-sjablonen nemen in plaats van deze onzin. Hoe kan iemand dat zeggen interface{} beter leesbaar of zelfs typveilig?

De ellende van afhankelijkheidsbeheer

Go heeft een ingebouwd afhankelijkheidssysteem dat bovenop populaire hostingproviders is gebouwd VCS. De tools die bij Go worden geleverd, kennen deze services en kunnen er in één klap code van downloaden, bouwen en installeren. Hoewel dit geweldig is, is er een groot probleem met versiebeheer! Ja, het is waar dat je de broncode van diensten als github of bitbucket kunt verkrijgen met behulp van Go-tools, maar je kunt de versie niet specificeren. En opnieuw gaat eenvoud ten koste van het nut. Ik kan de logica van een dergelijk besluit niet begrijpen.

Na vragen te hebben gesteld over een oplossing voor dit probleem, is het Go-ontwikkelteam ontstaan forumdraad, waarin werd uiteengezet hoe zij dit probleem zouden omzeilen. Hun aanbeveling was om op een dag eenvoudigweg de hele repository naar uw project te kopiëren en het “zoals het is” te laten. Wat denken ze in godsnaam? We hebben geweldige versiecontrolesystemen met geweldige tagging en versie-ondersteuning die de Go-makers negeren en gewoon de broncode kopiëren.

Culturele bagage van Xi

Naar mijn mening is Go ontwikkeld door mensen die C hun hele leven hebben gebruikt en door mensen die niet iets nieuws wilden proberen. De taal kan worden omschreven als C met extra wielen(oorsprong.: training wielen). Er zitten geen nieuwe ideeën in, behalve steun voor parallellisme (wat trouwens prachtig is) en dit is jammer. Je hebt een uitstekende parallelliteit in een nauwelijks bruikbare, flauwe taal.

Een ander krakend probleem is dat Go een procedurele taal is (zoals de stille horror van C). Uiteindelijk schrijf je code in een procedurele stijl die archaïsch en achterhaald aanvoelt. Ik weet dat objectgeoriënteerd programmeren geen wondermiddel is, maar het zou geweldig zijn om de details in typen te kunnen samenvatten en inkapseling te bieden.

Eenvoud voor uw eigen voordeel

Go is ontworpen om eenvoudig te zijn en slaagt daarin. Het is geschreven voor zwakke programmeurs, waarbij een oude taal als sjabloon werd gebruikt. Het wordt compleet geleverd met eenvoudige hulpmiddelen om eenvoudige dingen te doen. Het is gemakkelijk te lezen en gemakkelijk te gebruiken.

Het is extreem uitgebreid, niet indrukwekkend en slecht voor slimme programmeurs.

Dank meerinvald voor bewerkingen

Bron: www.habr.com

Voeg een reactie