De ce Go este rău pentru programatorii neinteligenti

Articolul a fost scris ca răspuns la un articol publicat anterior articol antipodean.

De ce Go este rău pentru programatorii neinteligenti

În ultimii doi ani, am folosit Go pentru a implementa un server RADIUS specializat cu un sistem de facturare dezvoltat. Pe parcurs, învăț complexitățile limbii în sine. Programele în sine sunt foarte simple și nu reprezintă scopul acestui articol, dar experiența utilizării Go în sine merită câteva cuvinte în apărarea sa. Go devine un limbaj din ce în ce mai popular pentru cod serios, scalabil. Limbajul a fost creat de Google, unde este folosit în mod activ. În concluzie, cred sincer că designul limbajului Go este rău pentru programatorii neinteligenti.

Proiectat pentru programatori slabi?

Cei slabi vorbesc despre probleme. Discuțiile puternice despre idei și vise...

Go este foarte ușor de învățat, atât de ușor încât poți citi codul practic fără antrenament. Această caracteristică a limbajului este utilizată în multe companii globale atunci când codul este citit împreună cu specialiști non-core (manageri, clienți etc.). Acest lucru este foarte convenabil pentru metodologii precum Design Driven Development.
Chiar și programatorii începători încep să producă cod destul de decent după o săptămână sau două. Cartea din care am studiat este „Go Programming” (de Mark Summerfield). Cartea este foarte bună, atinge multe nuanțe ale limbii. După limbaje complicate inutil, cum ar fi Java, PHP, lipsa magiei este revigorantă. Dar, mai devreme sau mai târziu, mulți programatori limitati au ideea de a folosi metode vechi într-un domeniu nou. Este chiar necesar acest lucru?

Rob Pike (principalul ideolog al limbii) a creat limbajul Go ca un limbaj industrial ușor de înțeles și eficient de utilizat. Limbajul este conceput pentru productivitate maximă în echipe mari și nu există nicio îndoială în acest sens. Mulți programatori începători se plâng că există multe caracteristici care le lipsesc. Această dorință de simplitate a fost o decizie conștientă a designerilor limbajului și, pentru a înțelege pe deplin de ce a fost nevoie, trebuie să înțelegem motivația dezvoltatorilor și ce încercau să realizeze în Go.

Deci de ce a fost făcut atât de simplu? Iată câteva citate din Rob Pike:

Punctul cheie aici este că programatorii noștri nu sunt cercetători. Sunt, de regulă, destul de tineri, vin la noi după ce au studiat, poate au studiat Java, sau C/C++, sau Python. Ei nu pot înțelege un limbaj grozav, dar în același timp dorim ca ei să creeze un software bun. De aceea, limba ar trebui să fie ușor de înțeles și de învățat.

Ar trebui să fie familiar, aproximativ similar cu C. Programatorii care lucrează la Google își încep cariera devreme și sunt în mare parte familiarizați cu limbajele procedurale, în special cu familia C. Cerința de productivitate rapidă într-un nou limbaj de programare înseamnă că limbajul nu ar trebui să fie prea radical.

Cuvinte înțelepte, nu-i așa?

Artefacte ale simplității

Simplitatea este o condiție necesară pentru frumusețe. Lev Tolstoi.

Menținerea simplității este unul dintre cele mai importante obiective în orice design. După cum știți, un proiect perfect nu este un proiect în care nu există nimic de adăugat, ci unul din care nu există nimic de eliminat. Mulți oameni cred că pentru a rezolva (sau chiar exprima) probleme complexe este nevoie de un instrument complex. Cu toate acestea, nu este. Să luăm limbajul PERL de exemplu. Ideologii limbajului credeau că un programator ar trebui să aibă cel puțin trei moduri diferite de a rezolva o problemă. Ideologii limbajului Go au luat o cale diferită, au decis că o singură cale, dar cu adevărat bună, era suficientă pentru a atinge scopul. Această abordare are o bază serioasă: singura cale este mai ușor de învățat și mai greu de uitat.

Mulți migranți se plâng că limba nu conține abstracții elegante. Da, este adevărat, dar acesta este unul dintre principalele avantaje ale limbii. Limbajul conține un minim de magie - deci nu sunt necesare cunoștințe profunde pentru a citi programul. În ceea ce privește verbozitatea codului, aceasta nu este deloc o problemă. Un program Golang bine scris citește pe verticală, cu o structură mică sau deloc. În plus, viteza de citire a unui program este cu cel puțin un ordin de mărime mai mare decât viteza de scriere a acestuia. Dacă considerați că tot codul are formatare uniformă (realizat folosind comanda încorporată gofmt), atunci citirea câtorva linii suplimentare nu este deloc o problemă.

Nu foarte expresiv

Arta nu tolerează atunci când libertatea ei este constrânsă. Acuratețea nu este responsabilitatea lui.

Datorită dorinței de simplitate, lui Go îi lipsesc constructele care în alte limbi sunt percepute ca ceva natural de către oamenii obișnuiți cu ele. La început poate fi oarecum incomod, dar apoi observi că programul este mult mai ușor și mai clar de citit.

De exemplu, un utilitar de consolă care citește stdin sau un fișier din argumentele liniei de comandă ar arăta astfel:

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

Soluția la aceeași problemă în D, deși pare ceva mai scurtă, nu este mai ușor de citit

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

Iadul de copiere

Omul poartă iadul în sine. Martin luther.

Începătorii se plâng în mod constant de Go în ceea ce privește lipsa genericelor. Pentru a rezolva această problemă, majoritatea folosesc copierea directă a codului. De exemplu, o funcție pentru însumarea unei liste de numere întregi, astfel de potențiali profesioniști consideră că funcționalitatea nu poate fi implementată în niciun alt mod decât prin simpla copiere-lipire pentru fiecare tip de date.

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

Limbajul are mijloace suficiente pentru a implementa astfel de construcții. De exemplu, programarea generică ar fi bine.

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

Și, deși codul nostru s-a dovedit a fi ceva mai lung decât cazul precedent, a devenit generalizat. Prin urmare, nu ne va fi dificil să implementăm toate operațiile aritmetice.

Mulți vor spune că un program în D arată semnificativ mai scurt și vor avea dreptate.

import std.stdio;
import std.algorithm;

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

Cu toate acestea, este doar mai scurt, dar nu mai corect, deoarece implementarea D ignoră complet problema gestionării erorilor.

În viața reală, pe măsură ce complexitatea logicii crește, decalajul se restrânge rapid. Decalajul se închide și mai rapid atunci când trebuie să efectuați o acțiune care nu poate fi efectuată folosind operatori în limbaj standard.

În ceea ce privește mentenabilitatea, extensibilitatea și lizibilitatea, în opinia mea, limbajul Go câștigă, deși pierde în verbozitate.

Programarea generalizată în unele cazuri ne oferă beneficii incontestabile. Acest lucru este ilustrat clar de pachetul de sortare. Deci, pentru a sorta orice listă, trebuie doar să implementăm interfața 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)
}

Dacă luați orice proiect open source și rulați comanda grep „interfață{}” -R, veți vedea cât de des sunt folosite interfețe confuze. Tovarășii apropiați vor spune imediat că toate acestea se datorează lipsei de generice. Cu toate acestea, acest lucru nu este întotdeauna cazul. Să luăm ca exemplu DELPHI. În ciuda prezenței acelorași generice, conține un tip VARIANT special pentru operațiuni cu tipuri de date arbitrare. Limbajul Go face același lucru.

De la un tun la vrăbii

Și cămașa de forță trebuie să se potrivească cu dimensiunea nebuniei. Stanislav Lec.

Mulți fani extremi ar putea pretinde că Go are un alt mecanism de creare a genericelor - reflecția. Și vor avea dreptate... dar numai în cazuri rare.

Rob Pike ne avertizează:

Acesta este un instrument puternic care trebuie folosit cu prudență. Ar trebui evitată dacă nu este strict necesar.

Wikipedia ne spune următoarele:

Reflecția se referă la procesul în timpul căruia un program își poate monitoriza și modifica propria structură și comportament în timpul execuției. Paradigma de programare care stă la baza reflecției se numește programare reflectivă. Acesta este un tip de metaprogramare.

Cu toate acestea, după cum știți, trebuie să plătiți pentru tot. In acest caz este:

  • dificultate în scrierea programelor
  • viteza de executare a programului

Prin urmare, reflexia trebuie folosită cu prudență, ca o armă de calibru mare. Folosirea neatenționată a reflexiei duce la programe imposibil de citit, erori constante și viteză redusă. Exact lucru pentru ca un programator snob să-și poată etala codul în fața altor colegi, mai pragmatici și mai modesti.

Bagaj cultural de la Xi? Nu, din mai multe limbi!

Odată cu averea se lasă și datoriile moștenitorilor.

În ciuda faptului că mulți cred că limba se bazează în întregime pe moștenirea C, acesta nu este cazul. Limbajul încorporează multe aspecte ale celor mai bune limbaje de programare.

sintaxă

În primul rând, sintaxa structurilor gramaticale se bazează pe sintaxa limbajului C. Cu toate acestea, limba DELPHI a avut și o influență semnificativă. Astfel, vedem că parantezele redundante, care reduc foarte mult lizibilitatea programului, au fost complet eliminate. Limbajul conține, de asemenea, operatorul „:=” inerent limbajului DELPHI. Conceptul de pachete este împrumutat din limbi precum ADA. Declarația entităților neutilizate este împrumutată din limbajul PROLOG.

Semantică

Pachetele au fost bazate pe semantica limbajului DELPHI. Fiecare pachet încapsulează date și cod și conține entități private și publice. Acest lucru vă permite să reduceți interfața pachetului la minimum.

Operațiunea de implementare prin metoda delegației a fost împrumutată din limbajul DELPHI.

Compilare

Nu fără motiv există o glumă: Go a fost dezvoltat în timp ce un program C era compilat. Unul dintre punctele forte ale limbajului este compilarea sa ultra-rapidă. Ideea a fost împrumutată din limba DELPHI. Fiecare pachet Go corespunde unui modul DELPHI. Aceste pachete sunt recompilate numai atunci când este cu adevărat necesar. Prin urmare, după următoarea editare, nu trebuie să compilați întregul program, ci mai degrabă să recompilați doar pachetele modificate și pachetele care depind de aceste pachete modificate (și chiar și atunci, doar dacă interfețele pachetelor s-au schimbat).

Construcții de nivel înalt

Limbajul conține multe constructe diferite de nivel înalt care nu sunt în niciun fel legate de limbaje de nivel scăzut precum C.

  • linii
  • Tabele de hash
  • felii
  • Duck typing este împrumutat din limbi precum RUBY (pe care, din păcate, mulți nu le înțeleg sau nu le folosesc la întregul său potențial).

Gestionarea memoriei

Gestionarea memoriei merită în general un articol separat. Dacă în limbaje precum C++, controlul este lăsat complet la latitudinea dezvoltatorului, atunci în limbile ulterioare precum DELPHI a fost folosit un model de numărare a referințelor. Cu această abordare, referințele ciclice nu au fost permise, deoarece s-au format clustere orfane, atunci Go are detectarea încorporată a unor astfel de clustere (cum ar fi C#). În plus, colectorul de gunoi este mai eficient decât majoritatea implementărilor cunoscute în prezent și poate fi deja folosit pentru multe sarcini în timp real. Limbajul în sine recunoaște situațiile în care o valoare pentru stocarea unei variabile poate fi alocată pe stivă. Acest lucru reduce sarcina managerului de memorie și crește viteza programului.

Concurență și concurență

Paralelismul și competitivitatea limbii sunt dincolo de laudă. Niciun limbaj de nivel scăzut nu poate concura de la distanță cu Go. Pentru a fi corect, este de remarcat faptul că modelul nu a fost inventat de autorii limbii, ci a fost pur și simplu împrumutat din vechiul limbaj ADA. Limbajul este capabil să proceseze milioane de conexiuni paralele folosind toate procesoarele, având în același timp probleme mai puțin complexe cu blocaje și condiții de cursă tipice pentru codul cu mai multe fire.

Beneficii aditionale

Dacă este profitabil, toată lumea va deveni altruistă.

Limbajul ne oferă, de asemenea, o serie de beneficii neîndoielnice:

  • Un singur fișier executabil după construirea proiectului simplifică foarte mult implementarea aplicațiilor.
  • Tastarea statică și inferența tipului pot reduce semnificativ numărul de erori din codul dvs., chiar și fără a scrie teste. Cunosc niște programatori care se descurcă deloc fără să scrie teste și calitatea codului lor nu are de suferit semnificativ.
  • Compilare încrucișată foarte simplă și portabilitate excelentă a bibliotecii standard, ceea ce simplifică foarte mult dezvoltarea aplicațiilor multiplatforme.
  • Expresiile regulate RE2 sunt sigure pentru fire și au timpi de execuție previzibili.
  • O bibliotecă standard puternică care permite majorității proiectelor să se descurce fără cadre terțe.
  • Limbajul este suficient de puternic pentru a se concentra mai degrabă asupra problemei decât asupra modului de rezolvare, dar suficient de scăzut pentru ca problema să poată fi rezolvată eficient.
  • Sistemul eco Go conține deja instrumente dezvoltate din cutie pentru toate ocaziile: teste, documentație, gestionarea pachetelor, linter puternice, generare de cod, detector de condiții de cursă etc.
  • Versiunea Go 1.11 a introdus gestionarea încorporată a dependenței semantice, construită pe baza găzduirii populare VCS. Toate instrumentele care alcătuiesc ecosistemul Go folosesc aceste servicii pentru a descărca, construi și instala codul din ele dintr-o singură lovitură. Și asta e grozav. Odată cu venirea versiunii 1.11, problema cu versiunea pachetului a fost de asemenea rezolvată complet.
  • Deoarece ideea de bază a limbajului este de a reduce magia, limbajul stimulează dezvoltatorii să gestioneze erorile în mod explicit. Și acest lucru este corect, deoarece în caz contrar, va uita pur și simplu de tratarea erorilor. Un alt lucru este că majoritatea dezvoltatorilor ignoră în mod deliberat gestionarea erorilor, preferând în loc să le proceseze să transmită pur și simplu eroarea în sus.
  • Limbajul nu implementează metodologia clasică OOP, deoarece în forma sa pură nu există virtualitate în Go. Cu toate acestea, aceasta nu este o problemă atunci când utilizați interfețe. Absența OOP reduce semnificativ bariera de intrare pentru începători.

Simplitate pentru beneficiul comunității

Este ușor de complicat, greu de simplificat.

Go a fost conceput pentru a fi simplu și reușește acest obiectiv. A fost scris pentru programatorii inteligenți care înțeleg beneficiile muncii în echipă și s-au săturat de variabilitatea nesfârșită a limbajelor la nivel de întreprindere. Având un set relativ mic de structuri sintactice în arsenalul său, practic nu este supus modificărilor în timp, astfel încât dezvoltatorii au mult timp eliberat pentru dezvoltare și nu pentru studierea la nesfârșit a inovațiilor lingvistice.

Companiile primesc și o serie de avantaje: o barieră scăzută la intrare le permite să găsească rapid un specialist, iar imuabilitatea limbajului le permite să folosească același cod chiar și după 10 ani.

Concluzie

Dimensiunea mare a creierului nu a făcut niciodată un elefant laureat al Premiului Nobel.

Pentru acei programatori al căror ego personal are prioritate față de spiritul de echipă, precum și pentru teoreticienii care iubesc provocările academice și „auto-îmbunătățirea” nesfârșită, limbajul este foarte rău, deoarece este un limbaj artizanal de uz general, care nu vă permite să obțineți placere estetica din rezultatul muncii tale si arata-te profesionist in fata colegilor (cu conditia sa masuram inteligenta dupa aceste criterii, si nu dupa IQ). Ca tot în viață, este o chestiune de priorități personale. Ca toate inovațiile valoroase, limbajul a parcurs deja un drum lung de la negarea universală la acceptarea în masă. Limbajul este ingenios prin simplitatea sa și, după cum știți, totul ingenios este simplu!

Sursa: www.habr.com

Adauga un comentariu