4 Go Critici de limbă
Publicat: 2022-03-11Go (alias Golang) este una dintre limbile care îi interesează cel mai mult pe oameni. În aprilie 2018, se află pe locul 19 în indexul TIOBE. Din ce în ce mai mulți oameni trec de la PHP, Node.js și alte limbi la Go și îl folosesc în producție. O mulțime de programe interesante (cum ar fi Kubernetes, Docker și Heroku CLI) sunt scrise folosind Go.
Deci, care este cheia succesului Go? Există o mulțime de lucruri în limbaj care o fac cu adevărat cool. Dar unul dintre principalele lucruri care au făcut ca Go să fie atât de popular este simplitatea sa, așa cum a subliniat unul dintre creatorii săi, Rob Pike.
Simplitatea este cool: nu trebuie să înveți multe cuvinte cheie. Face învățarea limbilor foarte ușoară și rapidă. Cu toate acestea, pe de altă parte, uneori, dezvoltatorilor le lipsesc unele caracteristici pe care le au în alte limbi și, prin urmare, trebuie să codifice soluții alternative sau să scrie mai mult cod pe termen lung. Din păcate, Go nu are o mulțime de caracteristici prin design și, uneori, este cu adevărat enervant.
Golang a fost menit să facă dezvoltarea mai rapidă, dar în multe situații, scrieți mai mult cod decât ați scrie folosind alte limbaje de programare. Voi descrie câteva astfel de cazuri în criticile mele în limba Go de mai jos.
Cele 4 Critici ale limbajului Go
1. Lipsa supraîncărcării funcțiilor și a valorilor implicite pentru argumente
Voi posta un exemplu de cod real aici. Când lucram la legarea lui Golang cu seleniu, trebuia să scriu o funcție care are trei parametri. Două dintre ele erau opționale. Iată cum arată după implementare:
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) }
A trebuit să implementez trei funcții diferite pentru că nu puteam pur și simplu supraîncărca funcția sau trece valorile implicite — Go nu o furnizează prin proiectare. Imaginează-ți ce s-ar întâmpla dacă aș suna din greșeală pe cel greșit? Iată un exemplu:
Trebuie să recunosc că uneori supraîncărcarea funcțiilor poate duce la cod dezordonat. Pe de altă parte, din cauza asta, programatorii trebuie să scrie mai mult cod.
Cum poate fi îmbunătățit?
Iată același (ei bine, aproape același) exemplu în JavaScript:
function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }
După cum puteți vedea, pare mult mai clar.
Îmi place și abordarea Elixir în acest sens. Iată cum ar arăta în Elixir (știu că aș putea folosi valori implicite, ca în exemplul de mai sus - o arăt doar ca o modalitate de a face acest lucru):
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. Lipsa genericelor
Aceasta este, fără îndoială, caracteristica pe care utilizatorii Go o cer cel mai mult.
Imaginați-vă că doriți să scrieți o funcție de hartă, în care treceți matricea de numere întregi și funcția, care va fi aplicată tuturor elementelor sale. Sună ușor, nu?
Să o facem pentru numere întregi:
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] }
Arată bine, nu?
Ei bine, imaginați-vă că trebuie să o faceți și pentru corzi. Va trebui să scrieți o altă implementare, care este exact aceeași, cu excepția semnăturii. Această funcție va avea nevoie de un nume diferit, deoarece Golang nu acceptă supraîncărcarea funcției. Ca rezultat, veți avea o grămadă de funcții similare cu nume diferite și va arăta cam așa:
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 }
Acest lucru contravine cu siguranță principiului DRY (Don’t Repeat Yourself), care prevede că trebuie să scrieți cât mai puțin cod de copiere/lipire posibil și, în schimb, să îl mutați în funcții și să le reutilizați.
O altă abordare ar fi să folosiți implementări unice cu interface{}
ca parametru, dar acest lucru poate duce la o eroare de rulare, deoarece verificarea tipului de rulare este mai predispusă la erori. Și, de asemenea, va fi mai lent, așa că nu există o modalitate simplă de a implementa aceste funcții ca una.
Cum poate fi îmbunătățit?
Există o mulțime de limbi bune care includ suport pentru generice. De exemplu, aici este același cod în Rust (am folosit vec
în loc de array
pentru a face mai simplu):
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_"] }
Rețineți că există o singură implementare a funcției de map
și poate fi folosită pentru orice tip de care aveți nevoie, chiar și pentru cele personalizate.
3. Managementul dependenței
Oricine are experiență în Go poate spune că gestionarea dependenței este foarte dificilă. Instrumentele Go permit utilizatorilor să instaleze diferite biblioteci prin rularea go get <library repo>
. Problema aici este gestionarea versiunilor. Dacă întreținătorul bibliotecii face unele modificări incompatibile cu înapoi și le încarcă în GitHub, oricine încearcă să vă folosească programul după aceea va primi o eroare, deoarece go get
nu face altceva decât să git clone
depozitul dvs. într-un folder de bibliotecă. De asemenea, dacă biblioteca nu este instalată, programul nu se va compila din această cauză.

Puteți face ceva mai bine folosind Dep pentru gestionarea dependențelor (https://github.com/golang/dep), dar problema aici este că fie stocați toate dependențele dvs. în depozit (ceea ce nu este bine, deoarece depozitul dvs. va conține nu numai codul dvs., ci mii și mii de linii de cod de dependență), sau doar stocați lista de pachete (dar din nou, dacă întreținătorul dependenței face o modificare incompatibilă cu înapoi, totul se va prăbuși).
Cum poate fi îmbunătățit?
Cred că exemplul perfect aici este Node.js (și JavaScript în general, presupun) și NPM. NPM este un depozit de pachete. Stochează diferite versiuni de pachete, așa că dacă aveți nevoie de o anumită versiune a unui pachet, nicio problemă - o puteți obține de acolo. De asemenea, unul dintre lucrurile din orice aplicație Node.js/JavaScript este fișierul package.json
. Aici, toate dependențele și versiunile lor sunt listate, astfel încât să le puteți instala pe toate (și să obțineți versiunile care cu siguranță funcționează cu codul dvs.) cu npm install
.
De asemenea, exemplele grozave de gestionare a pachetelor sunt RubyGems/Bundler (pentru pachetele Ruby) și Crates.io/Cargo (pentru bibliotecile Rust).
4. Tratarea erorilor
Gestionarea erorilor în Go este foarte simplă. În Go, practic, puteți returna mai multe valori din funcții, iar funcția poate returna o eroare. Ceva de genul:
err, value := someFunction(); if err != nil { // handle it somehow }
Acum imaginați-vă că trebuie să scrieți o funcție care face trei acțiuni care returnează o eroare. Va arata cam asa:
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; }
Există o mulțime de coduri repetabile aici, ceea ce nu este bun. Și cu funcții mari, poate fi și mai rău! Probabil veți avea nevoie de o tastă pe tastatură pentru asta:
Cum poate fi îmbunătățit?
Îmi place abordarea JavaScript în acest sens. Funcția poate arunca o eroare, iar tu o poți prinde. Luați în considerare exemplul:
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 }
Este mult mai clar și nu conține cod repetabil pentru gestionarea erorilor.
Lucrurile bune din Go
Deși Go are multe defecte prin design, are și câteva caracteristici foarte interesante.
1. Goroutine
Programarea asincronă a fost simplificată în Go. În timp ce programarea multithreading este de obicei dificilă în alte limbi, generarea unui fir nou și rularea funcției în el, astfel încât să nu blocheze firul curent, este foarte simplă:
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }
2. Instrumente care sunt incluse cu Go
În timp ce în alte limbaje de programare trebuie să instalați biblioteci/instrumente diferite pentru diferite sarcini (cum ar fi testarea, formatarea codului static etc.), există o mulțime de instrumente interesante care sunt deja incluse în Go în mod implicit, cum ar fi:
-
gofmt
- Un instrument pentru analiza codului static. În comparație cu JavaScript, unde trebuie să instalați o dependență suplimentară, cum ar fieslint
saujshint
, aici este inclus în mod implicit. Și programul nici măcar nu se va compila dacă nu scrieți cod în stilul Go (nefolosind variabile declarate, importând pachete neutilizate etc.). -
go test
- Un cadru de testare. Din nou, în comparație cu JavaScript, trebuie să instalați dependențe suplimentare pentru testare (Jest, Mocha, AVA etc.). Aici, este inclus în mod implicit. Și vă permite să faceți o mulțime de lucruri interesante în mod implicit, cum ar fi benchmarking, conversia codului din documentație în teste etc. -
godoc
- Un instrument de documentare. Este plăcut să îl includeți în instrumentele implicite. - Compilatorul în sine. Este incredibil de rapid, în comparație cu alte limbaje compilate!
3. Amână
Cred că aceasta este una dintre cele mai frumoase caracteristici ale limbii. Imaginați-vă că trebuie să scrieți o funcție care deschide trei fișiere. Și dacă ceva nu reușește, va trebui să închideți fișierele deschise existente. Dacă există o mulțime de construcții de genul acesta, va arăta ca o mizerie. Luați în considerare acest exemplu de pseudo-cod:
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; }
Pare complicat. Aici intervine defer
lui 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 */ }
După cum vedeți, dacă vom primi o eroare la deschiderea fișierului numărul trei, alte fișiere vor fi închise automat, deoarece instrucțiunile defer
sunt executate înainte de a reveni în ordine inversă. De asemenea, este plăcut să aveți deschiderea și închiderea fișierelor în același loc în loc de părți diferite ale unei funcții.
Concluzie
Nu am menționat toate lucrurile bune și rele din Go, ci doar pe cele pe care le consider cele mai bune și cele mai rele.
Go este într-adevăr unul dintre limbajele de programare interesante în uz curent și chiar are potențial. Ne oferă instrumente și funcții foarte interesante. Cu toate acestea, există o mulțime de lucruri care pot fi îmbunătățite acolo.
Dacă noi, ca dezvoltatori Go, vom implementa aceste modificări, va beneficia foarte mult comunitatea noastră, deoarece va face programarea cu Go mult mai plăcută.
Între timp, dacă încercați să vă îmbunătățiți testele cu Go, încercați să vă testați aplicația Go: Începeți corect de colegul Toptaler Gabriel Aszalos.