4 Krytyka języka Go
Opublikowany: 2022-03-11Go (aka Golang) to jeden z języków, którymi ludzie są najbardziej zainteresowani. W kwietniu 2018 r. zajmuje 19 miejsce w indeksie TIOBE. Coraz więcej osób przechodzi z PHP, Node.js i innych języków na Go i używa go w produkcji. Wiele fajnych programów (takich jak Kubernetes, Docker i Heroku CLI) jest napisanych przy użyciu Go.
Jaki jest więc klucz do sukcesu Go? W języku jest wiele rzeczy, które sprawiają, że jest naprawdę fajny. Ale jedną z głównych rzeczy, która sprawiła, że Go jest tak popularna, jest jej prostota, na co zwrócił uwagę jeden z jego twórców, Rob Pike.
Prostota jest fajna: nie musisz uczyć się wielu słów kluczowych. Dzięki temu nauka języka jest bardzo łatwa i szybka. Jednak z drugiej strony czasami programistom brakuje niektórych funkcji, które mają w innych językach, i dlatego muszą kodować obejścia lub pisać więcej kodu na dłuższą metę. Niestety, Go brakuje wielu funkcji z założenia, a czasami jest to naprawdę denerwujące.
Golang miał przyspieszyć rozwój, ale w wielu sytuacjach piszesz więcej kodu niż w innych językach programowania. Poniżej opiszę kilka takich przypadków w mojej krytyce dotyczącej języka Go.
4 krytyki języka Go
1. Brak przeciążania funkcji i wartości domyślnych argumentów
Zamieszczę tutaj prawdziwy przykład kodu. Kiedy pracowałem nad wiązaniem Golanga Selenium, musiałem napisać funkcję, która ma trzy parametry. Dwa z nich były opcjonalne. Oto jak to wygląda po wdrożeniu:
func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }
Musiałem zaimplementować trzy różne funkcje, ponieważ nie mogłem po prostu przeciążyć funkcji lub przekazać wartości domyślnych — Go nie zapewnia tego zgodnie z projektem. Wyobraź sobie, co by się stało, gdybym przypadkowo zadzwonił do złego? Oto przykład:
Muszę przyznać, że czasami przeciążenie funkcji może powodować bałagan w kodzie. Z drugiej strony z tego powodu programiści muszą pisać więcej kodu.
Jak można to ulepszyć?
Oto ten sam (no, prawie taki sam) przykład w JavaScript:
function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }
Jak widać, wygląda to znacznie jaśniej.
Podoba mi się również podejście Elixir w tym zakresie. Oto jak by to wyglądało w Elixirze (wiem, że mógłbym użyć wartości domyślnych, jak w powyższym przykładzie – pokazuję to tylko jako sposób, w jaki można to zrobić):
defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")
2. Brak generyków
Jest to prawdopodobnie funkcja, o którą użytkownicy Go najbardziej proszą.
Wyobraź sobie, że chcesz napisać funkcję mapującą, w której przekazujesz tablicę liczb całkowitych oraz funkcję, która zostanie zastosowana do wszystkich jej elementów. Brzmi łatwo, prawda?
Zróbmy to dla liczb całkowitych:
package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }
Wygląda dobrze, prawda?
Cóż, wyobraź sobie, że musisz to zrobić również dla smyczków. Będziesz musiał napisać inną implementację, która jest dokładnie taka sama, z wyjątkiem sygnatury. Ta funkcja będzie potrzebować innej nazwy, ponieważ Golang nie obsługuje przeciążania funkcji. W rezultacie będziesz mieć wiele podobnych funkcji o różnych nazwach i będzie wyglądać mniej więcej tak:
func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }
To zdecydowanie jest sprzeczne z zasadą DRY (Don't Repeat Yourself), która mówi, że musisz napisać jak najmniej kodu kopiuj/wklej, a zamiast tego przenieść go do funkcji i ponownie ich użyć.
Innym podejściem byłoby użycie pojedynczych implementacji z parametrem interface{}
, ale może to spowodować błąd w czasie wykonywania, ponieważ sprawdzanie typu w czasie wykonywania jest bardziej podatne na błędy. A także będzie wolniejszy, więc nie ma prostego sposobu na zaimplementowanie tych funkcji jako jednej.
Jak można to ulepszyć?
Istnieje wiele dobrych języków, które obsługują generyki. Na przykład, oto ten sam kod w Rust (użyłem vec
zamiast array
, aby to uprościć):
fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }
Zauważ, że istnieje pojedyncza implementacja funkcji map
i może być używana do dowolnych typów, nawet niestandardowych.
3. Zarządzanie zależnościami
Każdy, kto ma doświadczenie w Go, może powiedzieć, że zarządzanie zależnościami jest naprawdę trudne. Narzędzia Go umożliwiają użytkownikom instalowanie różnych bibliotek poprzez uruchomienie go get <library repo>
. Problemem jest tutaj zarządzanie wersjami. Jeśli opiekun biblioteki wprowadzi jakieś zmiany niezgodne z poprzednimi wersjami i prześle je do GitHub, każdy, kto spróbuje później użyć twojego programu, otrzyma błąd, ponieważ go get
nie robi nic poza git clone
repozytorium do folderu biblioteki. Również jeśli biblioteka nie jest zainstalowana, program nie skompiluje się z tego powodu.

Możesz zrobić trochę lepiej, używając Dep do zarządzania zależnościami (https://github.com/golang/dep), ale problem polega na tym, że albo przechowujesz wszystkie swoje zależności w swoim repozytorium (co nie jest dobre, ponieważ twoje repozytorium będzie zawierać nie tylko twój kod, ale tysiące wierszy kodu zależności) lub po prostu przechowywać listę pakietów (ale znowu, jeśli opiekun zależności dokona zmiany niezgodnej z poprzednimi wersjami, wszystko się zawiesi).
Jak można to ulepszyć?
Myślę, że idealnym przykładem jest tutaj Node.js (i ogólnie JavaScript, jak sądzę) i NPM. NPM to repozytorium pakietów. Przechowuje różne wersje pakietów, więc jeśli potrzebujesz określonej wersji pakietu, nie ma problemu — możesz ją pobrać stamtąd. Ponadto jedną z rzeczy w każdej aplikacji Node.js/JavaScript jest plik package.json
. Tutaj wymienione są wszystkie zależności i ich wersje, więc możesz je wszystkie zainstalować (i uzyskać wersje, które zdecydowanie działają z Twoim kodem) za pomocą npm install
.
Świetnymi przykładami zarządzania pakietami są również RubyGems/Bundler (dla pakietów Ruby) i Crates.io/Cargo (dla bibliotek Rust).
4. Obsługa błędów
Obsługa błędów w Go jest bardzo prosta. W Go zasadniczo możesz zwrócić wiele wartości z funkcji, a funkcja może zwrócić błąd. Coś takiego:
err, value := someFunction(); if err != nil { // handle it somehow }
Teraz wyobraź sobie, że musisz napisać funkcję, która wykonuje trzy akcje, które zwracają błąd. Będzie to wyglądać mniej więcej tak:
func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }
Jest tu dużo powtarzalnego kodu, co nie jest dobre. A przy dużych funkcjach może być jeszcze gorzej! Do tego prawdopodobnie będziesz potrzebować klawisza na klawiaturze:
Jak można to ulepszyć?
Podoba mi się podejście JavaScript do tego. Funkcja może zgłosić błąd i możesz go złapać. Rozważ przykład:
function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }
Jest o wiele bardziej przejrzysty i nie zawiera powtarzalnego kodu do obsługi błędów.
Dobre rzeczy w Go
Chociaż Go ma wiele wad z założenia, ma również kilka naprawdę fajnych funkcji.
1. Gorutyny
Programowanie asynchroniczne w Go stało się naprawdę proste. Podczas gdy programowanie wielowątkowe jest zwykle trudne w innych językach, tworzenie nowego wątku i uruchamianie w nim funkcji, aby nie blokowało bieżącego wątku, jest naprawdę proste:
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }
2. Narzędzia dołączone do Go
Podczas gdy w innych językach programowania musisz zainstalować różne biblioteki/narzędzia do różnych zadań (takich jak testowanie, formatowanie kodu statycznego itp.), istnieje wiele fajnych narzędzi, które są już domyślnie zawarte w Go, takie jak:
-
gofmt
- Narzędzie do statycznej analizy kodu. W porównaniu do JavaScript, gdzie musisz zainstalować dodatkową zależność, taką jakeslint
lubjshint
, tutaj jest ona domyślnie dołączona. A program nawet się nie skompiluje, jeśli nie napiszesz kodu w stylu Go (nie używając zadeklarowanych zmiennych, importując nieużywane pakiety itp.). -
go test
— platforma testowa. Znowu w porównaniu do JavaScriptu do testowania trzeba doinstalować dodatkowe zależności (Jest, Mocha, AVA itp.). Tutaj jest domyślnie dołączony. I domyślnie pozwala na robienie wielu fajnych rzeczy, takich jak benchmarking, konwersja kodu w dokumentacji na testy itp. -
godoc
— narzędzie dokumentacji. Fajnie jest mieć go w domyślnych narzędziach. - Sam kompilator. Jest niesamowicie szybki w porównaniu do innych kompilowanych języków!
3. Odroczyć
Myślę, że to jedna z najprzyjemniejszych cech języka. Wyobraź sobie, że musisz napisać funkcję, która otwiera trzy pliki. A jeśli coś zawiedzie, będziesz musiał zamknąć istniejące otwarte pliki. Jeśli będzie dużo takich konstrukcji, będzie to wyglądało jak bałagan. Rozważmy ten przykład pseudokodu:
function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }
Wygląda na skomplikowaną. W tym miejscu pojawia się defer
Go:
package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }
Jak widać, jeśli otrzymamy błąd podczas otwierania pliku numer trzy, inne pliki zostaną automatycznie zamknięte, ponieważ instrukcje defer
są wykonywane przed powrotem w odwrotnej kolejności. Poza tym fajnie jest mieć otwieranie i zamykanie plików w tym samym miejscu, a nie w różnych częściach funkcji.
Wniosek
Nie wymieniłem wszystkich dobrych i złych rzeczy w Go, tylko te, które uważam za najlepsze i najgorsze.
Go to naprawdę jeden z interesujących obecnie używanych języków programowania, który ma naprawdę duży potencjał. Zapewnia nam naprawdę fajne narzędzia i funkcje. Jest jednak wiele rzeczy, które można tam poprawić.
Jeśli my, jako programiści Go, wdrożymy te zmiany, przyniesie to wiele korzyści naszej społeczności, ponieważ znacznie uprzyjemni programowanie w Go.
W międzyczasie, jeśli próbujesz ulepszyć swoje testy za pomocą Go, wypróbuj Testing Your Go App: Get Started Right Way autorstwa innego Toptalera, Gabriela Aszalosa.