Varför Go är dåligt för osmarta programmerare

Artikeln skrevs som ett svar på en tidigare publicerad antipodisk artikel.

Varför Go är dåligt för osmarta programmerare

Under de senaste två åren har jag använt Go för att implementera en specialiserad RADIUS-server med ett utvecklat faktureringssystem. Längs vägen lär jag mig invecklarna i själva språ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 till sitt försvar. Go blir ett allt mer vanligt 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 Ointelligenta programmerare.

Designad för svaga programmerare?

De svaga talar om problem. Det starka samtalet om idéer och drömmar...

Go är väldigt lätt att lära sig, så lätt att du kan läsa koden med praktiskt taget ingen träning alls. Denna funktion i språket används i många globala företag när koden läses tillsammans med icke-kärnspecialister (chefer, kunder, etc.). Detta är mycket praktiskt för metoder som Design Driven Development.
Även nybörjare programmerare börjar producera ganska anständig kod efter en vecka eller två. Boken jag studerade från är "Go Programming" (av Mark Summerfield). Boken är mycket bra, den berör många nyanser av språket. Efter onödigt komplicerade språk som Java, PHP är bristen på magi uppfriskande. Men förr eller senare har många begränsade programmerare idén om att använda gamla metoder på ett nytt område. Är detta verkligen nödvändigt?

Rob Pike (språkets främsta ideolog) skapade Go-språket som ett industrispråk som är lätt att förstå och effektivt att använda. Språket är designat för maximal produktivitet i stora team och det råder ingen tvekan om det. Många nybörjare klagar över att det finns många funktioner som de saknar. 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 från Rob Pike:

Det viktiga här är att våra programmerare inte är 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. Därför ska språket vara lätt 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.

Kloka ord, eller hur?

Artefakter av enkelhet

Enkelhet är ett nödvändigt villkor för skönhet. Lev Tolstoj.

Att hålla det enkelt är ett av de viktigaste målen i varje design. Som ni vet är ett perfekt projekt inte ett projekt där det inte finns något att tillägga, utan ett som det inte finns något att ta bort från. Många tror att för att lösa (eller till och med uttrycka) komplexa problem behövs ett komplext verktyg. Det är det dock inte. Låt oss ta PERL-språket till exempel. Språkideologer ansåg att en programmerare borde ha minst tre olika sätt att lösa ett problem. Go-språkets ideologer tog en annan väg, de bestämde att ett sätt, men ett riktigt bra, var tillräckligt för att uppnå målet. Detta tillvägagångssätt har en seriös grund: det enda sättet är lättare att lära sig och svårare att glömma.

Många migranter klagar på att språket inte innehåller eleganta abstraktioner. Ja, det är sant, men detta är en av de främsta fördelarna med språket. Språket innehåller ett minimum av magi - så det krävs inga djupa kunskaper för att läsa programmet. När det gäller kodens utförlighet är detta inte ett problem alls. Ett välskrivet Golang-program läser vertikalt, med liten eller ingen struktur. Dessutom är hastigheten för att läsa ett program åtminstone en storleksordning högre än hastigheten för att skriva det. Om du anser att all kod har enhetlig formatering (görs med det inbyggda gofmt-kommandot), så är det inte ett problem att läsa några extra rader.

Inte särskilt uttrycksfull

Konst tolererar inte när dess frihet är begränsad. Noggrannhet är inte hans ansvar.

På grund av önskan om enkelhet saknar Go konstruktioner som på andra språk uppfattas som något naturligt av människor som är vana vid dem. Till en början kan det vara något obekvämt, men sedan märker man att programmet är mycket lättare och mer entydigt att läsa.

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

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

Lösningen på samma problem i D, även om den ser något kortare ut, är inte lättare att läsa

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

Helvetet med kopiering

Människan bär helvetet inom sig. Martin Luther.

Nybörjare klagar ständigt på Go när det gäller bristen på generika. För att lösa detta problem använder de flesta av dem direkt kodkopiering. Till exempel en funktion för att summera en lista med heltal, sådana blivande proffs tror att funktionaliteten inte kan implementeras på något annat sätt än genom att enkelt kopiera och klistra in för varje datatyp.

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

Språket har tillräckliga medel för att implementera sådana konstruktioner. Till exempel skulle generisk programmering vara bra.

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

Och även om vår kod visade sig vara något längre än det tidigare fallet, har den blivit generaliserad. Därför kommer det inte att vara svårt för oss att implementera alla aritmetiska operationer.

Många kommer att säga att ett program i D ser betydligt kortare ut, och de kommer att ha rätt.

import std.stdio;
import std.algorithm;

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

Den är dock bara kortare, men inte mer korrekt, eftersom D-implementeringen helt ignorerar problemet med felhantering.

I det verkliga livet, när logikens komplexitet ökar, minskar klyftan snabbt. Mellanrummet sluts ännu snabbare när du behöver utföra en åtgärd som inte kan utföras med vanliga språkoperatorer.

När det gäller underhåll, töjbarhet och läsbarhet vinner enligt min mening Go-språket, även om det tappar i mångfald.

Generaliserad programmering ger oss i vissa fall obestridliga fördelar. Detta illustreras tydligt av sorteringspaketet. Så för att sortera vilken lista som helst behöver vi bara implementera sort.Interface-gränssnittet.

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

Om du tar något projekt med öppen källkod och kör kommandot grep "interface{}" -R, kommer du att se hur ofta förvirrande gränssnitt används. Nära kamrater kommer omedelbart att säga att allt detta beror på bristen på generika. Detta är dock inte alltid fallet. Låt oss ta DELPHI som ett exempel. Trots förekomsten av samma generika innehåller den en speciell VARIANT-typ för operationer med godtyckliga datatyper. Go-språket gör samma sak.

Från en kanon till sparvar

Och tvångströjan måste passa storleken på galenskapen. Stanislav Lec.

Många extrema fans kan hävda att Go har en annan mekanism för att skapa generika - reflektion. Och de kommer att ha rätt... men bara i sällsynta fall.

Rob Pike varnar oss:

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.

Wikipedia berättar följande:

Reflektion hänvisar till den process under vilken ett program kan övervaka och modifiera sin egen struktur och sitt beteende under exekvering. Det programmeringsparadigm som ligger bakom reflektion kallas reflekterande programmering. Detta är en typ av metaprogrammering.

Men som bekant måste du betala för allt. I det här fallet är det:

  • svårigheter att skriva program
  • programexekveringshastighet

Därför måste reflektion användas med försiktighet, som ett vapen med stor kaliber. Tanklöst användande av reflektion leder till oläsbara program, ständiga fel och låg hastighet. Precis grejen för en snobbprogrammerare att kunna visa upp sin kod inför andra, mer pragmatiska och blygsamma kollegor.

Kulturbagage från Xi? Nej, från ett antal språk!

Tillsammans med förmögenheten lämnas även skulder till arvingarna.

Trots att många tror att språket helt och hållet bygger på C-arvet så är det inte så. Språket innehåller många aspekter av de bästa programmeringsspråken.

syntax

Först och främst är syntaxen för grammatiska strukturer baserad på syntaxen för C-språket. Men DELPHI-språket hade också ett betydande inflytande. Därmed ser vi att de redundanta parenteserna, som kraftigt minskar programmets läsbarhet, har tagits bort helt. Språket innehåller också operatorn ":=" som är inbyggd i DELPHI-språket. Konceptet med paket är lånat från språk som ADA. Deklarationen av oanvända enheter är lånad från PROLOG-språket.

Semantik

Paketen var baserade på DELPHI-språkets semantik. Varje paket kapslar in data och kod och innehåller privata och offentliga enheter. Detta gör att du kan reducera paketgränssnittet till ett minimum.

Metoden för implementering genom delegering lånades från DELPHI-språket.

Kompilering

Det är inte utan anledning att det finns ett skämt: Go utvecklades medan ett C-program kompilerades. En av styrkorna med språket är dess ultrasnabba sammanställning. Idén lånades från DELPHI-språket. Varje Go-paket motsvarar en DELPHI-modul. Dessa paket kompileras om endast när det verkligen är nödvändigt. Efter nästa redigering behöver du därför inte kompilera hela programmet utan snarare kompilera om endast de ändrade paketen och paketen som är beroende av dessa ändrade paket (och även då, bara om paketgränssnitten har ändrats).

Konstruktioner på hög nivå

Språket innehåller många olika högnivåkonstruktioner som inte på något sätt är relaterade till lågnivåspråk som C.

  • linjer
  • Hash tabeller
  • Skivor
  • Ducktyping är lånat från språk som RUBY (som tyvärr många inte förstår eller använder till sin fulla potential).

Minneshantering

Minneshantering förtjänar i allmänhet en separat artikel. Om i språk som C++, kontrollen överlåts helt till utvecklaren, så användes en referensräkningsmodell på senare språk som DELPHI. Med detta tillvägagångssätt var cykliska referenser inte tillåtna, eftersom föräldralösa kluster bildades, då har Go inbyggd detektering av sådana kluster (som C#). Dessutom är sopsamlaren mer effektiv än de flesta för närvarande kända implementeringar och kan redan användas för många realtidsuppgifter. Språket själv känner igen situationer när ett värde för att lagra en variabel kan allokeras på stacken. Detta minskar belastningen på minneshanteraren och ökar programmets hastighet.

Samtidighet och samtidighet

Språkets parallellitet och konkurrenskraft är bortom beröm. Inget lågnivåspråk kan konkurrera med Go på distans. För att vara rättvis är det värt att notera att modellen inte uppfanns av språkets författare, utan helt enkelt lånad från det gamla goda ADA-språket. Språket kan bearbeta miljontals parallella anslutningar med alla processorer, samtidigt som det har en storleksordning mindre komplexa problem med dödlägen och rasförhållanden som är typiska för flertrådig kod.

Ytterligare fördelar

Om det är lönsamt kommer alla att bli osjälviska.

Språket ger oss också ett antal otvivelaktiga fördelar:

  • En enda körbar fil efter att projektet har byggts förenklar driftsättningen av applikationer avsevärt.
  • Statisk typning och typinferens kan avsevärt minska antalet fel i din kod, även utan att skriva tester. Jag känner en del programmerare som klarar sig utan att skriva test alls och kvaliteten på deras kod blir inte nämnvärt lidande.
  • Mycket enkel korskompilering och utmärkt portabilitet av standardbiblioteket, vilket avsevärt förenklar utvecklingen av plattformsoberoende applikationer.
  • RE2 reguljära uttryck är trådsäkra och har förutsägbara körtider.
  • Ett kraftfullt standardbibliotek som låter de flesta projekt klara sig utan ramverk från tredje part.
  • Språket är kraftfullt nog att fokusera på problemet snarare än hur man löser det, men ändå tillräckligt lågt för att problemet kan lösas effektivt.
  • Go eco-systemet innehåller redan utvecklade verktyg ur kartongen för alla tillfällen: tester, dokumentation, pakethantering, kraftfulla linters, kodgenerering, tävlingsdetektor, etc.
  • Go version 1.11 introducerade inbyggd semantisk beroendehantering, byggd ovanpå populära VCS-värd. Alla verktyg som utgör Go-ekosystemet använder dessa tjänster för att ladda ner, bygga och installera kod från dem i ett svep. Och det är jättebra. Med ankomsten av version 1.11 var problemet med paketversionering också helt löst.
  • Eftersom kärnidén med språket är att minska magi, uppmuntrar språket utvecklare att explicit göra felhantering. Och detta är korrekt, för annars kommer den helt enkelt att glömma felhanteringen helt och hållet. En annan sak är att de flesta utvecklare medvetet ignorerar felhantering och föredrar istället för att bearbeta dem att helt enkelt vidarebefordra felet uppåt.
  • Språket implementerar inte den klassiska OOP-metoden, eftersom det i sin rena form inte finns någon virtualitet i Go. Detta är dock inget problem när man använder gränssnitt. Frånvaron av OOP minskar avsevärt inträdesbarriären för nybörjare.

Enkelhet för samhällsnytta

Det är lätt att komplicera, svårt att förenkla.

Go designades för att vara enkelt och det lyckas med det målet. Den skrevs för smarta programmerare som förstår fördelarna med lagarbete och som är trötta på den oändliga variationen av språk på företagsnivå. Med en relativt liten uppsättning syntaktiska strukturer i sin arsenal är den praktiskt taget inte föremål för förändringar över tiden, så utvecklare har mycket tid frigjort för utveckling, och inte för att oändligt studera språkinnovationer.

Företag får också ett antal fördelar: en låg inträdesbarriär gör att de snabbt kan hitta en specialist, och språkets oföränderlighet gör att de kan använda samma kod även efter 10 år.

Slutsats

Stor hjärnstorlek har aldrig gjort någon elefant till Nobelpristagare.

För de programmerare vars personliga ego har företräde framför laganda, såväl som teoretiker som älskar akademiska utmaningar och oändliga "självförbättringar", är språket riktigt dåligt, eftersom det är ett allmänt hantverksspråk som inte tillåter dig att få estetisk glädje av resultatet av ditt arbete och visa dig professionell inför kollegor (förutsatt att vi mäter intelligens med dessa kriterier och inte med IQ). Som allt i livet är det en fråga om personliga prioriteringar. Som alla sevärda innovationer har språket redan kommit långt från universellt förnekande till massacceptans. Språket är genialiskt i sin enkelhet, och som ni vet är allt genialiskt enkelt!

Källa: will.com

Lägg en kommentar