Dlaczego Go jest złe dla nieinteligentnych programistów

Artykuł powstał jako odpowiedź na wcześniej opublikowany artykuł antypodyjski.

Dlaczego Go jest złe dla nieinteligentnych programistów

Przez ostatnie ponad dwa lata korzystałem z Go do wdrożenia specjalistycznego serwera RADIUS z rozbudowanym systemem bilingowym. Przy okazji poznaję zawiłości samego języka. Same programy są bardzo proste i nie są celem tego artykułu, ale doświadczenie korzystania z samego Go zasługuje na kilka słów na swoją obronę. Go staje się coraz bardziej popularnym językiem dla poważnego, skalowalnego kodu. Język został stworzony przez Google i jest aktywnie używany. Podsumowując, szczerze uważam, że projekt języka Go jest zły dla programistów UNintelligent.

Zaprojektowany dla słabych programistów?

Słabi mówią o problemach. Mocna rozmowa o pomysłach i marzeniach...

Go jest bardzo łatwy do nauczenia, tak łatwy, że możesz przeczytać kod praktycznie bez żadnego szkolenia. Ta cecha języka jest wykorzystywana w wielu globalnych firmach, gdy kod jest czytany wspólnie ze specjalistami spoza branży (menedżerowie, klienci itp.). Jest to bardzo wygodne w przypadku metodologii takich jak rozwój oparty na projektowaniu.
Nawet początkujący programiści zaczynają tworzyć całkiem przyzwoity kod po tygodniu lub dwóch. Książka, z której się uczyłem, to „Go Programming” (autor: Mark Summerfield). Książka jest bardzo dobra, dotyka wielu niuansów języka. Po niepotrzebnie skomplikowanych językach typu Java, PHP, brak magii działa orzeźwiająco. Ale prędzej czy później wielu ograniczonych programistów wpada na pomysł wykorzystania starych metod w nowej dziedzinie. Czy to naprawdę konieczne?

Rob Pike (główny ideolog języka) stworzył język Go jako język przemysłowy, łatwy do zrozumienia i skuteczny w użyciu. Język został zaprojektowany z myślą o maksymalnej produktywności w dużych zespołach i nie ma co do tego wątpliwości. Wielu początkujących programistów narzeka, że ​​brakuje im wielu funkcji. To pragnienie prostoty było świadomą decyzją projektantów języka i aby w pełni zrozumieć, dlaczego była ona potrzebna, musimy zrozumieć motywację programistów i to, co próbowali osiągnąć w Go.

Dlaczego więc zrobiono to tak prosto? Oto kilka cytatów Roba Pike’a:

Kluczową kwestią jest to, że nasi programiści nie są badaczami. Są to z reguły dość młodzi ludzie, przychodzą do nas po studiach, być może uczyli się Javy, C/C++ lub Pythona. Nie rozumieją świetnego języka, ale jednocześnie chcemy, żeby tworzyli dobre oprogramowanie. Dlatego język powinien być łatwy do zrozumienia i nauki.

Powinien być znajomy, z grubsza podobny do C. Programiści pracujący w Google wcześnie rozpoczynają karierę i znają głównie języki proceduralne, w szczególności rodzinę C. Wymóg szybkiej produktywności w nowym języku programowania oznacza, że ​​język ten nie powinien być zbyt radykalny.

Mądre słowa, prawda?

Artefakty prostoty

Prostota jest warunkiem koniecznym piękna. Lew Tołstoj.

Utrzymanie prostoty jest jednym z najważniejszych celów każdego projektu. Jak wiadomo, idealny projekt to nie taki projekt, w którym nie można nic dodać, ale taki, od którego nie można nic odjąć. Wiele osób uważa, że ​​aby rozwiązać (a nawet wyrazić) złożone problemy, potrzebne jest złożone narzędzie. Jednak tak nie jest. Weźmy na przykład język PERL. Ideolodzy językowi uważali, że programista powinien mieć co najmniej trzy różne sposoby rozwiązania jednego problemu. Ideolodzy języka Go poszli inną drogą i uznali, że do osiągnięcia celu wystarczy jeden, ale naprawdę dobry sposób. To podejście ma poważne podstawy: jedyny sposób jest łatwiejszy do nauczenia i trudniejszy do zapomnienia.

Wielu migrantów skarży się, że język nie zawiera eleganckich abstrakcji. Tak, to prawda, ale jest to jedna z głównych zalet tego języka. Język zawiera minimum magii - więc do przeczytania programu nie jest wymagana głęboka wiedza. Jeśli chodzi o szczegółowość kodu, nie stanowi to żadnego problemu. Dobrze napisany program Golang czyta pionowo, z niewielką strukturą lub bez niej. Ponadto prędkość czytania programu jest co najmniej o rząd wielkości większa niż prędkość jego pisania. Jeśli wziąć pod uwagę, że cały kod ma jednolite formatowanie (wykonane za pomocą wbudowanego polecenia gofmt), to przeczytanie kilku dodatkowych linii nie stanowi żadnego problemu.

Mało wyraziste

Sztuka nie toleruje, gdy jej wolność jest ograniczana. Dokładność nie jest jego obowiązkiem.

Ze względu na dążenie do prostoty w Go brakuje konstrukcji, które w innych językach są odbierane przez przyzwyczajonych do nich ludzi jako coś naturalnego. Na początku może to być nieco niewygodne, ale potem zauważysz, że program jest znacznie łatwiejszy i bardziej jednoznaczny w czytaniu.

Na przykład narzędzie konsoli odczytujące stdin lub plik z argumentów wiersza poleceń wyglądałoby następująco:

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

Rozwiązanie tego samego problemu w D, choć wygląda na nieco krótsze, nie jest łatwiejsze do odczytania

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

Piekło kopiowania

Człowiek nosi w sobie piekło. Marcin Luther.

Początkujący stale narzekają na Go z powodu braku leków generycznych. Aby rozwiązać ten problem, większość z nich korzysta z bezpośredniego kopiowania kodu. Na przykład funkcja sumowania listy liczb całkowitych, potencjalni profesjonaliści uważają, że tej funkcjonalności nie da się zaimplementować w inny sposób niż poprzez proste kopiowanie i wklejanie dla każdego typu danych.

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

Język ma wystarczające środki do realizacji takich konstrukcji. Na przykład programowanie ogólne byłoby w porządku.

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 choć nasz kod okazał się nieco dłuższy niż w poprzednim przypadku, uległ uogólnieniu. Dlatego nie będzie nam trudno zrealizować wszystkie operacje arytmetyczne.

Wielu powie, że program w D wygląda na znacznie krótszy i będą mieli rację.

import std.stdio;
import std.algorithm;

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

Jest jednak tylko krótsza, ale nie bardziej poprawna, ponieważ implementacja D całkowicie ignoruje problem obsługi błędów.

W prawdziwym życiu, wraz ze wzrostem złożoności logiki, różnica szybko się zmniejsza. Luka zamyka się jeszcze szybciej, gdy trzeba wykonać czynność, której nie można wykonać przy użyciu standardowych operatorów języka.

Pod względem łatwości konserwacji, rozszerzalności i czytelności, moim zdaniem, język Go wygrywa, choć traci w gadatliwości.

Uogólnione programowanie w niektórych przypadkach daje nam niezaprzeczalne korzyści. Widać to wyraźnie na przykładzie pakietu sort. Aby posortować dowolną listę, wystarczy zaimplementować interfejs 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)
}

Jeśli weźmiesz dowolny projekt open source i uruchomisz polecenie grep „interface{}” -R, zobaczysz, jak często używane są mylące interfejsy. Towarzysze o zamkniętych umysłach natychmiast powiedzą, że wszystko to wynika z braku leków generycznych. Jednak nie zawsze tak jest. Weźmy na przykład firmę DELPHI. Pomimo obecności tych samych typów ogólnych, zawiera specjalny typ VARIANT do operacji na dowolnych typach danych. Język Go robi to samo.

Od armaty do wróbli

A kaftan bezpieczeństwa musi pasować do rozmiaru szaleństwa. Stanisław Lec.

Wielu fanów ekstremalnych rozwiązań może twierdzić, że Go ma jeszcze jeden mechanizm tworzenia generycznych rozwiązań – refleksję. I będą mieli rację... ale tylko w rzadkich przypadkach.

Rob Pike ostrzega nas:

To potężne narzędzie, którego należy używać ostrożnie. Należy tego unikać, jeśli nie jest to absolutnie konieczne.

Wikipedia mówi nam co następuje:

Refleksja odnosi się do procesu, podczas którego program może monitorować i modyfikować swoją własną strukturę oraz zachowanie podczas wykonywania. Paradygmat programowania leżący u podstaw refleksji nazywany jest programowaniem refleksyjnym. Jest to rodzaj metaprogramowania.

Jednak jak wiadomo za wszystko trzeba zapłacić. W tym przypadku jest to:

  • trudności w pisaniu programów
  • szybkość wykonywania programu

Dlatego odbicia należy używać ostrożnie, jak przy użyciu broni dużego kalibru. Bezmyślne użycie refleksji prowadzi do nieczytelnych programów, ciągłych błędów i niskiej prędkości. W sam raz dla programisty-snoba, żeby mógł pochwalić się swoim kodem przed innymi, bardziej pragmatycznymi i skromnymi kolegami.

Bagaż kulturowy Xi? Nie, z wielu języków!

Wraz z majątkiem spadkobiercom zostają pozostawione także długi.

Pomimo tego, że wielu uważa, że ​​język ten w całości opiera się na dziedzictwie C, tak nie jest. Język zawiera wiele aspektów najlepszych języków programowania.

składnia

Przede wszystkim składnia struktur gramatycznych opiera się na składni języka C. Jednak język DELPHI również miał znaczący wpływ. Widzimy zatem, że całkowicie usunięto zbędne nawiasy, które znacznie zmniejszają czytelność programu. Język zawiera także operator „:=” charakterystyczny dla języka DELPHI. Pojęcie pakietów zostało zapożyczone z języków takich jak ADA. Deklaracja niewykorzystanych bytów jest zapożyczona z języka PROLOG.

Semantyka

Pakiety zostały oparte na semantyce języka DELPHI. Każdy pakiet zawiera dane i kod oraz zawiera podmioty prywatne i publiczne. Pozwala to na ograniczenie interfejsu pakietu do minimum.

Operację implementacji metodą delegacji zapożyczono z języka DELPHI.

Kompilacja

Nie bez powodu jest taki żart: Go powstał w czasie kompilacji programu w C. Jedną z mocnych stron tego języka jest jego ultraszybka kompilacja. Pomysł został zapożyczony z języka DELPHI. Każdy pakiet Go odpowiada modułowi DELPHI. Pakiety te są rekompilowane tylko wtedy, gdy jest to naprawdę konieczne. Dlatego po kolejnej edycji nie musisz kompilować całego programu, ale raczej przekompilować tylko zmienione pakiety i pakiety, które zależą od tych zmienionych pakietów (i nawet wtedy, tylko jeśli zmieniły się interfejsy pakietów).

Konstrukcje na wysokim poziomie

Język zawiera wiele różnych konstrukcji wysokiego poziomu, które nie są w żaden sposób powiązane z językami niskiego poziomu, takimi jak C.

  • Linie
  • Tabele mieszające
  • Plasterki
  • Pisanie na klawiaturze jest zapożyczone z języków takich jak RUBY (którego niestety wielu nie rozumie lub nie wykorzystuje w pełni jego potencjału).

Zarządzanie pamięcią

Zarządzanie pamięcią w zasadzie zasługuje na osobny artykuł. Jeśli w językach takich jak C++ kontrolę pozostawiono całkowicie programiście, to w późniejszych językach, takich jak DELPHI, zastosowano model liczenia referencji. Przy takim podejściu niedozwolone były odniesienia cykliczne, ponieważ powstały klastry osierocone, wówczas Go ma wbudowaną funkcję wykrywania takich klastrów (jak C#). Ponadto moduł zbierający śmieci jest bardziej wydajny niż większość obecnie znanych implementacji i może być już używany do wielu zadań w czasie rzeczywistym. Sam język rozpoznaje sytuacje, gdy na stosie można przydzielić wartość do przechowywania zmiennej. Zmniejsza to obciążenie menedżera pamięci i zwiększa szybkość programu.

Współbieżność i współbieżność

Paralelizm i konkurencyjność języka jest nie do pochwały. Żaden język niskiego poziomu nie może nawet w najmniejszym stopniu konkurować z Go. Gwoli ścisłości warto zaznaczyć, że model ten nie został wymyślony przez autorów języka, lecz został po prostu zapożyczony ze starego, dobrego języka ADA. Język jest w stanie przetwarzać miliony równoległych połączeń przy użyciu wszystkich procesorów, mając jednocześnie o rząd wielkości mniej złożone problemy z zakleszczeniami i warunkami wyścigu typowymi dla kodu wielowątkowego.

Dodatkowe korzyści

Jeśli będzie to opłacalne, wszyscy staną się bezinteresowni.

Język zapewnia nam także szereg niewątpliwych korzyści:

  • Pojedynczy plik wykonywalny po zbudowaniu projektu znacznie ułatwia wdrażanie aplikacji.
  • Typowanie statyczne i wnioskowanie o typie mogą znacznie zmniejszyć liczbę błędów w kodzie, nawet bez pisania testów. Znam kilku programistów, którzy w ogóle radzą sobie bez pisania testów i jakość ich kodu nie ucierpi znacząco.
  • Bardzo prosta kompilacja krzyżowa i doskonała przenośność standardowej biblioteki, co znacznie upraszcza tworzenie aplikacji wieloplatformowych.
  • Wyrażenia regularne RE2 są bezpieczne dla wątków i mają przewidywalny czas wykonania.
  • Potężna biblioteka standardowa, która pozwala większości projektów obejść się bez frameworków innych firm.
  • Język jest na tyle potężny, że pozwala skupić się na problemie, a nie na tym, jak go rozwiązać, a jednocześnie na tyle niskim poziomem, że problem można skutecznie rozwiązać.
  • System Go eco zawiera już gotowe narzędzia na każdą okazję: testy, dokumentacja, zarządzanie pakietami, wydajne lintery, generowanie kodu, wykrywanie warunków wyścigowych itp.
  • Wersja Go 1.11 wprowadziła wbudowane zarządzanie zależnościami semantycznymi, zbudowane na popularnym hostingu VCS. Wszystkie narzędzia tworzące ekosystem Go korzystają z tych usług, aby za jednym zamachem pobierać, kompilować i instalować z nich kod. I to jest świetne. Wraz z pojawieniem się wersji 1.11 problem z wersjonowaniem pakietów został również całkowicie rozwiązany.
  • Ponieważ podstawową ideą języka jest ograniczenie magii, język zachęca programistów do jawnej obsługi błędów. I jest to poprawne, ponieważ w przeciwnym razie po prostu całkowicie zapomni o obsłudze błędów. Inną rzeczą jest to, że większość programistów celowo ignoruje obsługę błędów, woląc zamiast je przetwarzać, po prostu przesyłać błąd w górę.
  • Język nie implementuje klasycznej metodologii OOP, ponieważ w czystej postaci w Go nie ma wirtualności. Nie stanowi to jednak problemu w przypadku korzystania z interfejsów. Brak OOP znacząco zmniejsza barierę wejścia dla początkujących.

Prostota z korzyścią dla społeczności

Łatwo jest skomplikować, trudno uprościć.

Go został zaprojektowany tak, aby był prosty i udało mu się osiągnąć ten cel. Został napisany dla inteligentnych programistów, którzy rozumieją korzyści płynące z pracy zespołowej i są zmęczeni nieskończoną różnorodnością języków na poziomie korporacyjnym. Mając w swoim arsenale stosunkowo niewielki zestaw struktur syntaktycznych, praktycznie nie podlega zmianom w czasie, więc programiści mają dużo czasu na rozwój, a nie na niekończące się studiowanie innowacji językowych.

Firmy zyskują także szereg korzyści: niska bariera wejścia pozwala szybko znaleźć specjalistę, a niezmienność języka pozwala na używanie tego samego kodu nawet po 10 latach.

wniosek

Duży rozmiar mózgu nigdy nie uczynił żadnego słonia zdobywcą Nagrody Nobla.

Dla programistów, których osobiste ego ma pierwszeństwo przed duchem zespołowym, a także teoretyków, którzy kochają akademickie wyzwania i niekończące się „samodoskonalenie”, język jest naprawdę zły, ponieważ jest to język rzemieślniczy ogólnego przeznaczenia, który nie pozwala na zdobycie estetyczną przyjemność z wyniku swojej pracy i pokaż się profesjonalistom przed kolegami (pod warunkiem, że inteligencję mierzymy tymi kryteriami, a nie IQ). Jak wszystko w życiu, jest to kwestia osobistych priorytetów. Jak wszystkie wartościowe innowacje, język ten przeszedł już długą drogę od powszechnego zaprzeczenia do masowej akceptacji. Język jest genialny w swojej prostocie, a jak wiadomo wszystko, co genialne, jest proste!

Źródło: www.habr.com

Dodaj komentarz