Varför Go Design är dåligt för smarta programmerare

Under de senaste månaderna har jag använt Go för implementeringar. Bevis på koncept (cirka.: kod för att testa en idés funktionalitet) på fritiden, dels för att studera själva programmeringsspråket. Programmen i sig är väldigt enkla och är inte syftet med den här artikeln, men själva upplevelsen av att använda Go förtjänar några ord om det. Go lovar att bli (cirka.: artikel skriven 2015) ett populärt språk för seriös skalbar kod. Språket skapades av Google, där det används aktivt. Sammanfattningsvis tror jag ärligt talat att designen av Go-språket är dåligt för smarta programmerare.

Designad för svaga programmerare?

Go är väldigt lätt att lära sig, så lätt att introduktionen tog mig en kväll, varefter jag redan kunde koda produktivt. Boken jag brukade lära mig Go heter En introduktion till programmering i Go (översättning), är den tillgänglig online. Boken är liksom själva Go-källkoden lättläst, har bra kodexempel och innehåller cirka 150 sidor som kan läsas på en gång. Denna enkelhet är uppfriskande till en början, särskilt i en programmeringsvärld fylld av alltför komplicerad teknik. Men till slut uppstår förr eller senare tanken: "Är det verkligen så?"

Google hävdar att Gos enkelhet är dess försäljningsargument och språket är designat för maximal produktivitet i stora team, men jag tvivlar på det. Det finns funktioner som antingen saknas eller är alltför detaljerade. Och allt på grund av bristande förtroende för utvecklare, med antagandet att de inte kan göra något rätt. Denna önskan om enkelhet var ett medvetet beslut av språkets designers, och för att fullt ut förstå varför det behövdes måste vi förstå utvecklarnas motivation och vad de försökte uppnå i Go.

Så varför gjordes det så enkelt? Här är ett par citat Rob Pike (cirka.: en av medskaparna av Go-språket):

Nyckelpunkten här är att våra programmerare (cirka.: Googlers) är inte forskare. De är som regel ganska unga, kom till oss efter att ha studerat, kanske har de studerat Java, eller C/C++ eller Python. De kan inte förstå ett bra språk, men samtidigt vill vi att de ska skapa bra mjukvara. Det är därför deras språk ska vara lätt för dem att förstå och lära sig.
 
Han borde vara bekant, ungefär som C. Programmerare som arbetar på Google börjar sin karriär tidigt och är mestadels bekanta med procedurspråk, särskilt C-familjen. Kravet på snabb produktivitet i ett nytt programmeringsspråk gör att språket inte ska vara för radikalt.

Vad? Så Rob Pike säger i princip att utvecklarna på Google inte är så bra, det är därför de skapade ett språk för idioter (cirka.: dumbed down) så att de kan göra något. Vad är det för arrogant blick på dina egna kollegor? Jag har alltid trott att Googles utvecklare är handplockade bland de smartaste och bästa på jorden. Visst klarar de något svårare?

Artefakter av överdriven enkelhet

Att vara enkel är ett värdigt mål i vilken design som helst, och att försöka göra något enkelt är svårt. Men när man försöker lösa (eller till och med uttrycka) komplexa problem, behövs ibland ett komplext verktyg. Komplexitet och intrikat är inte de bästa egenskaperna hos ett programmeringsspråk, men det finns en mellanväg där språket kan skapa eleganta abstraktioner som är lätta att förstå och använda.

Inte särskilt uttrycksfull

På grund av sitt engagemang för enkelhet saknar Go konstruktioner som uppfattas som naturliga på andra språk. Detta kan tyckas vara en bra idé till en början, men i praktiken resulterar det i utförlig kod. Anledningen till detta borde vara uppenbar – det måste vara lätt för utvecklare att läsa andras kod, men i själva verket skadar dessa förenklingar bara läsbarheten. Det finns inga förkortningar i Go: antingen mycket eller ingenting.

Till exempel skulle ett konsolverktyg som läser stdin eller en fil från kommandoradsargument se ut så här:

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

Även om den här koden också försöker vara så allmän som möjligt, kommer Gos påtvingade verbositet i vägen, och som ett resultat resulterar lösandet av ett enkelt problem i en stor mängd kod.

Här finns till exempel en lösning på samma problem i 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);
    }
}

Och vem är mer läsbar nu? Jag ger min röst till D. Hans kod är mycket mer läsbar eftersom han beskriver handlingarna tydligare. D använder mycket mer komplexa begrepp (cirka.: alternativt funktionsanrop и mallar) än i Go-exemplet, men det är egentligen inget komplicerat med att förstå dem.

Helvetet med kopiering

Ett populärt förslag för att förbättra Go är generalitet. Detta kommer åtminstone att hjälpa till att undvika onödig kopiering av kod för att stödja alla datatyper. Till exempel kan en funktion för att summera en lista med heltal implementeras på inget annat sätt än genom att kopiera in dess grundläggande funktion för varje heltalstyp; det finns inget annat sätt:

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

Och det här exemplet fungerar inte ens för signerade typer. Detta tillvägagångssätt bryter helt mot principen om att inte upprepa dig själv (TORR), en av de mest kända och uppenbara principerna, och ignorerar vilken som är källan till många fel. Varför gör Go detta? Detta är en fruktansvärd aspekt av språket.

Samma exempel på D:

import std.stdio;
import std.algorithm;

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

Enkelt, elegant och rakt på sak. Funktionen som används här är reduce för malltyp och predikat. Ja, det här är återigen mer komplicerat än Go-versionen, men inte så svårt för smarta programmerare att förstå. Vilket exempel är lättare att underhålla och lättare att läsa?

Enkel typ av systembypass

Jag föreställer mig att Go-programmerare som läser detta kommer att skumma om munnen och skrika, "Du gör det fel!" Tja, det finns ett annat sätt att göra en generisk funktion och typer, men det bryter helt typsystemet!

Ta en titt på det här exemplet på en dum språkfix för att komma runt problemet:

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

Denna implementering Reduce lånades från artikeln Idiomatisk generika i Go (cirka.: Jag kunde inte hitta översättningen, jag blir glad om du hjälper till med detta). Tja, om det är idiomatiskt, skulle jag hata att se ett icke-idiomatiskt exempel. Användande interface{} - en fars, och på språket behövs det bara för att kringgå maskinskrivning. Detta är ett tomt gränssnitt och alla typer implementerar det, vilket ger fullständig frihet för alla. Den här programmeringsstilen är fruktansvärt ful, och det är inte allt. Akrobatiska prestationer som dessa kräver användning av runtime-reflektion. Även Rob Pike gillar inte individer som missbrukar detta, som han nämnde i en av sina rapporter.

Detta är ett kraftfullt verktyg som bör användas med försiktighet. Det bör undvikas om det inte är absolut nödvändigt.

Jag skulle ta D-mallar istället för detta nonsens. Hur kan någon säga det interface{} mer läsbar eller till och med typsäker?

Beroendehanteringens elände

Go har ett inbyggt beroendesystem byggt ovanpå populära värdleverantörer VCS. Verktygen som följer med Go känner till dessa tjänster och kan ladda ner, bygga och installera kod från dem i ett svep. Även om det här är bra, finns det ett stort fel med versionshantering! Ja, det är sant att du kan få källkoden från tjänster som github eller bitbucket med hjälp av Go-verktyg, men du kan inte ange versionen. Och återigen enkelhet på bekostnad av användbarheten. Jag kan inte förstå logiken i ett sådant beslut.

Efter att ha ställt frågor om en lösning på detta problem skapade Go-utvecklingsteamet forumtråd, som beskrev hur de skulle komma runt denna fråga. Deras rekommendation var att helt enkelt kopiera hela förvaret till ditt projekt en dag och lämna det "som det är". Vad fan tänker de på? Vi har fantastiska versionskontrollsystem med bra taggning och versionsstöd som Go-skaparna ignorerar och bara kopierar källkoden.

Kulturbagage från Xi

Enligt mig har Go utvecklats av människor som använt C hela sitt liv och av de som inte ville testa något nytt. Språket kan beskrivas som C med extra hjul(ursprung.: stödhjul). Det finns inga nya idéer i den, förutom stöd för parallellism (vilket för övrigt är underbart) och det är synd. Du har utmärkt parallellitet i ett knappt användbart, haltande språk.

Ett annat knarrande problem är att Go är ett procedurspråk (som den tysta skräcken i C). Det slutar med att du skriver kod i en procedurstil som känns ålderdomlig och föråldrad. Jag vet att objektorienterad programmering inte är en silverkula, men det skulle vara bra att kunna abstrahera detaljerna till typer och ge inkapsling.

Enkelhet för din egen fördel

Go designades för att vara enkelt och det lyckas med det målet. Den skrevs för svaga programmerare, med ett gammalt språk som mall. Den levereras komplett med enkla verktyg för att göra enkla saker. Det är lätt att läsa och lätt att använda.

Det är extremt mångsidigt, föga imponerande och dåligt för smarta programmerare.

Tack mersinvald för redigeringar

Källa: will.com

Lägg en kommentar