Zakaj je Go slab za nepametne programerje

Članek je nastal kot odgovor na že objavljeno antipodni člen.

Zakaj je Go slab za nepametne programerje

V zadnjih dveh in več letih sem uporabljal Go za implementacijo specializiranega strežnika RADIUS z razvitim sistemom obračunavanja. Na tej poti se učim zapletenosti samega jezika. Sami programi so zelo preprosti in niso namen tega članka, vendar si sama izkušnja uporabe Go zasluži nekaj besed v svojo obrambo. Go postaja vse bolj priljubljen jezik za resno, razširljivo kodo. Jezik je ustvaril Google, kjer se aktivno uporablja. Pod črto, iskreno mislim, da je zasnova jezika Go slaba za NEinteligentne programerje.

Zasnovan za šibke programerje?

Šibki govorijo o težavah. Močan govor o idejah in sanjah...

Go se je zelo enostavno naučiti, tako enostavno, da lahko kodo preberete tako rekoč brez vadbe. Ta značilnost jezika se uporablja v številnih svetovnih podjetjih, ko kodo berejo skupaj s stranskimi strokovnjaki (vodje, stranke itd.). To je zelo priročno za metodologije, kot je Design Driven Development.
Tudi programerji začetniki začnejo izdelovati povsem spodobno kodo po tednu ali dveh. Knjiga, po kateri sem študiral, je »Pojdi na programiranje« (avtor Mark Summerfield). Knjiga je zelo dobra, dotika se mnogih nians jezika. Po nepotrebno zapletenih jezikih, kot sta Java, PHP, je pomanjkanje čarovnije osvežujoče. Toda prej ali slej se številni omejeni programerji zamislijo o uporabi starih metod na novem področju. Je to res potrebno?

Rob Pike (glavni ideolog jezika) je ustvaril jezik Go kot industrijski jezik, ki je enostaven za razumevanje in učinkovit za uporabo. Jezik je zasnovan za maksimalno produktivnost v velikih skupinah in o tem ni dvoma. Mnogi programerji začetniki se pritožujejo, da jim manjka veliko funkcij. Ta želja po preprostosti je bila zavestna odločitev oblikovalcev jezika in da bi popolnoma razumeli, zakaj je bil potreben, moramo razumeti motivacijo razvijalcev in kaj so poskušali doseči v Go.

Zakaj je bilo torej narejeno tako preprosto? Tukaj je nekaj citatov Roba Pikea:

Ključno pri tem je, da naši programerji niso raziskovalci. Praviloma so precej mladi, pridejo k nam po študiju, morda so študirali Javo, ali C/C++ ali Python. Ne morejo razumeti odličnega jezika, a hkrati želimo, da ustvarijo dobro programsko opremo. Zato mora biti jezik enostaven za razumevanje in učenje.

Moral bi biti znan, grobo rečeno podoben C. Programerji, ki delajo pri Googlu, svojo kariero začnejo zgodaj in večinoma poznajo proceduralne jezike, zlasti družino C. Zahteva po hitri produktivnosti v novem programskem jeziku pomeni, da jezik ne sme biti preveč radikalen.

Pametne besede, kajne?

Artefakti preprostosti

Preprostost je nujen pogoj za lepoto. Lev Tolstoj.

Enostavnost je eden najpomembnejših ciljev pri vsakem oblikovanju. Kot veste, popoln projekt ni projekt, pri katerem ni kaj dodati, ampak tisti, iz katerega ni ničesar odstraniti. Mnogi ljudje verjamejo, da je za reševanje (ali celo izražanje) kompleksnih problemov potrebno kompleksno orodje. Vendar pa ni. Vzemimo za primer jezik PERL. Jezikovni ideologi so menili, da mora imeti programer vsaj tri različne načine za rešitev enega problema. Ideologi jezika Go so ubrali drugačno pot, odločili so, da je za dosego cilja dovolj že ena, a res dobra pot. Ta pristop ima resne temelje: edino tako se je lažje naučiti in težje pozabiti.

Mnogi migranti se pritožujejo, da jezik ne vsebuje elegantnih abstrakcij. Da, to je res, vendar je to ena glavnih prednosti jezika. Jezik vsebuje najmanj magije - zato za branje programa ni potrebno poglobljeno znanje. Kar zadeva podrobnost kode, to sploh ni problem. Dobro napisan program Golang se bere navpično, z malo ali brez strukture. Poleg tega je hitrost branja programa vsaj za red velikosti večja od hitrosti pisanja. Če menite, da ima vsa koda enotno oblikovanje (izvedeno z vgrajenim ukazom gofmt), potem branje nekaj dodatnih vrstic sploh ni problem.

Ni zelo ekspresivno

Umetnost ne prenese, ko je njena svoboda omejena. Natančnost ni njegova odgovornost.

Zaradi želje po enostavnosti Go nima konstruktov, ki jih v drugih jezikih ljudje, ki so jih vajeni, dojemajo kot nekaj naravnega. Sprva je morda nekoliko neprijetno, potem pa opazite, da je program veliko lažji in bolj nedvoumen za branje.

Na primer, pripomoček konzole, ki bere stdin ali datoteko iz argumentov ukazne vrstice, bi bil videti takole:

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

Rešitev istega problema v D, čeprav je videti nekoliko krajša, ni nič lažja za branje

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

Hudičevo kopiranje

Človek nosi pekel v sebi. Martin Luter.

Začetniki se nenehno pritožujejo nad Go v smislu pomanjkanja generikov. Za rešitev te težave jih večina uporablja neposredno kopiranje kode. Na primer funkcija za seštevanje seznama celih števil, takšni morebitni strokovnjaki menijo, da funkcionalnosti ni mogoče implementirati na noben drug način kot s preprostim kopiranjem in lepljenjem za vsako vrsto podatkov.

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

Jezik ima dovolj sredstev za izvajanje takšnih konstrukcij. Na primer, generično programiranje bi bilo v redu.

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

In čeprav se je naša koda izkazala za nekoliko daljšo od prejšnjega primera, je postala posplošena. Zato nam ne bo težko izvajati vseh aritmetičnih operacij.

Mnogi bodo rekli, da je program v D videti bistveno krajši, in imeli bodo prav.

import std.stdio;
import std.algorithm;

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

Je pa le krajši, ne pa tudi pravilnejši, saj implementacija D popolnoma ignorira problem obravnavanja napak.

V resničnem življenju, ko se kompleksnost logike povečuje, se vrzel hitro zmanjšuje. Vrzel se zapre še hitreje, ko morate izvesti dejanje, ki ga ni mogoče izvesti z operatorji standardnega jezika.

Kar zadeva vzdržljivost, razširljivost in berljivost, po mojem mnenju jezik Go zmaga, čeprav izgubi v besednosti.

Splošno programiranje nam v nekaterih primerih daje nesporne prednosti. To je jasno razvidno iz paketa za razvrščanje. Torej, da razvrstimo kateri koli seznam, moramo samo implementirati vmesnik sort.Interface.

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

Če vzamete kateri koli odprtokodni projekt in zaženete ukaz grep “interface{}” -R, boste videli, kako pogosto se uporabljajo zavajajoči vmesniki. Bližnji tovariši bodo takoj rekli, da je vse to posledica pomanjkanja generičnih zdravil. Vendar ni vedno tako. Vzemimo za primer DELPHI. Kljub prisotnosti istih generičnih podatkov vsebuje poseben tip VARIANT za operacije s poljubnimi tipi podatkov. Jezik Go počne enako.

Od topa do vrabcev

In prisilni jopič mora ustrezati velikosti norosti. Stanislav Lec.

Mnogi ekstremni oboževalci lahko trdijo, da ima Go še en mehanizem za ustvarjanje generikov - refleksijo. In imeli bodo prav ... a le v redkih primerih.

Rob Pike nas opozarja:

To je močno orodje, ki ga je treba uporabljati previdno. Izogibati se je treba, razen če je to nujno potrebno.

Wikipedia nam pove naslednje:

Refleksija se nanaša na proces, med katerim lahko program spremlja in spreminja lastno strukturo in obnašanje med izvajanjem. Paradigma programiranja, na kateri temelji refleksija, se imenuje refleksivno programiranje. To je vrsta metaprogramiranja.

Vendar, kot veste, morate plačati za vse. V tem primeru je:

  • težave pri pisanju programov
  • hitrost izvajanja programa

Zato je treba refleksijo uporabljati previdno, kot orožje velikega kalibra. Nepremišljena uporaba refleksije vodi v neberljive programe, stalne napake in nizko hitrost. Prav za programerja snoba, da se lahko pohvali s svojo kodo pred drugimi, bolj pragmatičnimi in skromnimi kolegi.

Kulturna prtljaga iz Xija? Ne, iz številnih jezikov!

Skupaj z bogastvom dedičem ostanejo tudi dolgovi.

Kljub temu, da mnogi menijo, da jezik v celoti temelji na dediščini C, temu ni tako. Jezik vključuje številne vidike najboljših programskih jezikov.

sintaksa

Prvič, sintaksa slovničnih struktur temelji na sintaksi jezika C. Pomemben vpliv pa je imel tudi jezik DELPHI. Tako vidimo, da so v celoti odstranjeni odvečni oklepaji, ki močno zmanjšujejo berljivost programa. Jezik vsebuje tudi operator “:=”, ki je lasten jeziku DELPHI. Koncept paketov je izposojen iz jezikov, kot je ADA. Deklaracija neuporabljenih entitet je izposojena iz jezika PROLOG.

Semantika

Paketi so temeljili na semantiki jezika DELPHI. Vsak paket vsebuje podatke in kodo ter vsebuje zasebne in javne subjekte. To vam omogoča zmanjšanje vmesnika paketa na minimum.

Operacija implementacije z metodo delegiranja je bila izposojena iz jezika DELPHI.

Sestavljanje

Ni brez razloga, da obstaja šala: Go je bil razvit med prevajanjem programa C. Ena od prednosti jezika je njegovo izjemno hitro prevajanje. Ideja je bila izposojena iz jezika DELPHI. Vsak paket Go ustreza modulu DELPHI. Ti paketi se znova prevedejo le, ko je to res potrebno. Zato vam po naslednjem urejanju ni treba prevesti celotnega programa, temveč znova prevesti samo spremenjene pakete in pakete, ki so odvisni od teh spremenjenih paketov (pa še to, samo če so se vmesniki paketov spremenili).

Konstrukti na visoki ravni

Jezik vsebuje veliko različnih konstruktov na visoki ravni, ki nikakor niso povezani z jeziki na nizki ravni, kot je C.

  • Strune
  • Zgoščene tabele
  • Rezine
  • Duck tipkanje je izposojeno iz jezikov, kot je RUBY (ki ga na žalost mnogi ne razumejo ali uporabljajo v celoti).

Upravljanje pomnilnika

Upravljanje pomnilnika si na splošno zasluži ločen članek. Če je v jezikih, kot je C++, nadzor v celoti prepuščen razvijalcu, potem je bil v kasnejših jezikih, kot je DELPHI, uporabljen model štetja referenc. S tem pristopom ciklične reference niso bile dovoljene, ker so nastale gruče osirotele, potem ima Go vgrajeno zaznavanje takšnih gruč (kot C#). Poleg tega je zbiralnik smeti učinkovitejši od večine trenutno znanih izvedb in ga je že mogoče uporabiti za številne naloge v realnem času. Jezik sam prepozna situacije, ko je mogoče skladu dodeliti vrednost za shranjevanje spremenljivke. To zmanjša obremenitev upravljalnika pomnilnika in poveča hitrost programa.

Sočasnost in sočasnost

Vzporednost in tekmovalnost jezika je za vsako pohvalo. Noben jezik na nizki ravni ne more niti približno tekmovati z Go. Po pravici povedano je treba omeniti, da modela niso izumili avtorji jezika, ampak so ga preprosto izposodili iz dobrega starega jezika ADA. Jezik je sposoben obdelati milijone vzporednih povezav z uporabo vseh procesorjev, hkrati pa ima za red velikosti manj zapletene težave z zastoji in tekmovalnimi pogoji, ki so značilni za večnitno kodo.

Dodatne ugodnosti

Če je donosno, bodo vsi postali nesebični.

Jezik nam prinaša tudi številne nedvomne prednosti:

  • Ena sama izvršljiva datoteka po izdelavi projekta močno poenostavi uvajanje aplikacij.
  • Statično tipkanje in sklepanje o tipih lahko znatno zmanjšata število napak v vaši kodi, tudi brez pisanja testov. Poznam nekaj programerjev, ki sploh ne pišejo testov in kakovost njihove kode ne trpi bistveno.
  • Zelo enostavno navzkrižno prevajanje in odlična prenosljivost standardne knjižnice, kar močno poenostavi razvoj večplatformskih aplikacij.
  • Regularni izrazi RE2 so varni za niti in imajo predvidljive čase izvajanja.
  • Zmogljiva standardna knjižnica, ki omogoča večini projektov brez okvirov tretjih oseb.
  • Jezik je dovolj zmogljiv, da se osredotoči na problem in ne na to, kako ga rešiti, vendar dovolj nizek, da je problem mogoče učinkovito rešiti.
  • Sistem Go eco že vsebuje razvita orodja za vse priložnosti: teste, dokumentacijo, upravljanje paketov, zmogljive linterje, generiranje kode, detektor dirkalnih pogojev itd.
  • Različica Go 1.11 je predstavila vgrajeno upravljanje semantične odvisnosti, zgrajeno na vrhu priljubljenega gostovanja VCS. Vsa orodja, ki sestavljajo ekosistem Go, uporabljajo te storitve za prenos, gradnjo in namestitev kode iz njih z enim zamahom. In to je super. S prihodom različice 1.11 je bila popolnoma odpravljena tudi težava z verzioniranjem paketov.
  • Ker je osnovna ideja jezika zmanjšati magijo, jezik spodbuja razvijalce k eksplicitnemu obravnavanju napak. In to je prav, saj bo v nasprotnem primeru preprosto popolnoma pozabil na obravnavanje napak. Druga stvar je, da večina razvijalcev namenoma ignorira obravnavo napak in namesto da bi jih obdelali, raje preprosto posreduje napako navzgor.
  • Jezik ne izvaja klasične OOP metodologije, saj v čisti obliki v Go ni virtualnosti. Vendar to ni problem pri uporabi vmesnikov. Odsotnost OOP bistveno zmanjša vstopno oviro za začetnike.

Preprostost v korist skupnosti

Lahko je komplicirati, težko je poenostaviti.

Go je zasnovan tako, da je preprost in ta cilj mu tudi uspe. Napisana je bila za pametne programerje, ki razumejo prednosti timskega dela in so utrujeni od neskončne variabilnosti jezikov na ravni podjetja. Ker ima v svojem arzenalu razmeroma majhen nabor sintaktičnih struktur, se sčasoma praktično ne spreminja, zato imajo razvijalci veliko časa, ki se sprosti za razvoj in ne za neskončno preučevanje jezikovnih novosti.

Podjetja so deležna tudi številnih prednosti: nizka vstopna ovira jim omogoča hitro iskanje strokovnjaka, nespremenljivost jezika pa jim omogoča uporabo iste kode tudi po 10 letih.

Zaključek

Zaradi velikih možganov noben slon nikoli ni bil Nobelov nagrajenec.

Za tiste programerje, katerih osebni ego ima prednost pred skupinskim duhom, pa tudi za teoretike, ki ljubijo akademske izzive in neskončno "samoizboljševanje", je jezik res slab, saj gre za obrtniški jezik za splošno uporabo, ki ne omogoča, da bi dobili estetski užitek od rezultatov svojega dela in se pred sodelavci pokazati profesionalnim (pod pogojem, da inteligenco merimo po teh merilih in ne po IQ). Kot vse v življenju je tudi to stvar osebnih prioritet. Kot vse vredne inovacije je jezik že prehodil dolgo pot od vsesplošnega zanikanja do množičnega sprejemanja. Jezik je genialen v svoji preprostosti in, kot veste, je vse genialno preprosto!

Vir: www.habr.com

Dodaj komentar