4 Vai Critiche linguistiche
Pubblicato: 2022-03-11Go (aka Golang) è una delle lingue a cui le persone sono più interessate. Ad aprile 2018, si trova al 19° posto nell'indice TIOBE. Sempre più persone stanno passando da PHP, Node.js e altri linguaggi a Go e lo utilizzano in produzione. Molti software interessanti (come Kubernetes, Docker e Heroku CLI) vengono scritti utilizzando Go.
Allora, qual è la chiave del successo di Go? Ci sono molte cose all'interno del linguaggio che lo rendono davvero interessante. Ma una delle cose principali che ha reso Go così popolare è la sua semplicità, come sottolineato da uno dei suoi creatori, Rob Pike.
La semplicità è fantastica: non è necessario imparare molte parole chiave. Rende l'apprendimento delle lingue molto facile e veloce. Tuttavia, d'altra parte, a volte gli sviluppatori non dispongono di alcune funzionalità che hanno in altri linguaggi e, pertanto, devono codificare soluzioni alternative o scrivere più codice a lungo termine. Sfortunatamente, Go non ha molte funzionalità in base alla progettazione e talvolta è davvero fastidioso.
Golang doveva rendere lo sviluppo più veloce, ma in molte situazioni stai scrivendo più codice di quello che scriveresti usando altri linguaggi di programmazione. Descriverò alcuni di questi casi nelle mie critiche alla lingua Go di seguito.
Le 4 critiche linguistiche
1. Mancanza di sovraccarico delle funzioni e valori predefiniti per gli argomenti
Pubblicherò un esempio di codice reale qui. Quando stavo lavorando sull'associazione al selenio di Golang, avevo bisogno di scrivere una funzione che avesse tre parametri. Due di loro erano facoltativi. Ecco come appare dopo l'implementazione:
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) }Ho dovuto implementare tre diverse funzioni perché non potevo semplicemente sovraccaricare la funzione o passare i valori predefiniti: Go non lo fornisce in base alla progettazione. Immagina cosa accadrebbe se per sbaglio chiamassi quello sbagliato? Ecco un esempio:
Devo ammettere che a volte il sovraccarico delle funzioni può causare codice disordinato. D'altra parte, a causa di ciò, i programmatori devono scrivere più codice.
Come può essere migliorato?
Ecco lo stesso (beh, quasi lo stesso) esempio in JavaScript:
function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }Come puoi vedere, sembra molto più chiaro.
Mi piace anche l'approccio Elisir su questo. Ecco come apparirebbe in Elixir (so che potrei usare i valori predefiniti, come nell'esempio sopra, lo sto solo mostrando come un modo per farlo):
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. Mancanza di generici
Questa è probabilmente la funzione che gli utenti di Go chiedono di più.
Immagina di voler scrivere una funzione mappa, in cui stai passando l'array di numeri interi e la funzione, che verrà applicata a tutti i suoi elementi. Sembra facile, vero?
Facciamolo per numeri interi:
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] }Sembra buono, giusto?
Bene, immagina di doverlo fare anche per gli archi. Dovrai scrivere un'altra implementazione, che è esattamente la stessa tranne che per la firma. Questa funzione avrà bisogno di un nome diverso, poiché Golang non supporta l'overloading delle funzioni. Di conseguenza, avrai un sacco di funzioni simili con nomi diversi e assomiglierà a questo:
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 }Ciò va decisamente contro il principio DRY (Don't Repeat Yourself), che afferma che è necessario scrivere il minor numero possibile di codice copia/incolla e invece spostarlo in funzioni e riutilizzarle.
Un altro approccio sarebbe quello di utilizzare singole implementazioni con l' interface{} come parametro, ma ciò può comportare un errore di runtime perché il controllo del tipo di runtime è più soggetto a errori. E inoltre sarà più lento, quindi non esiste un modo semplice per implementare queste funzioni come una sola.
Come può essere migliorato?
Ci sono molti buoni linguaggi che includono il supporto per i generici. Ad esempio, ecco lo stesso codice in Rust (ho usato vec invece di array per renderlo più semplice):
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_"] } Nota che esiste un'unica implementazione della funzione map e può essere utilizzata per tutti i tipi di cui hai bisogno, anche quelli personalizzati.
3. Gestione delle dipendenze
Chiunque abbia esperienza in Go può dire che la gestione delle dipendenze è davvero difficile. Gli strumenti Go consentono agli utenti di installare diverse librerie eseguendo go get <library repo> . Il problema qui è la gestione della versione. Se il manutentore della libreria apporta alcune modifiche incompatibili con le versioni precedenti e lo carica su GitHub, chiunque tenti di utilizzare il tuo programma in seguito riceverà un errore, perché go get non fa altro che git clone il tuo repository in una cartella della libreria. Inoltre, se la libreria non è installata, il programma non verrà compilato per questo motivo.

Puoi fare leggermente meglio usando Dep per la gestione delle dipendenze (https://github.com/golang/dep), ma il problema qui è che stai archiviando tutte le tue dipendenze sul tuo repository (il che non va bene, perché il tuo repository lo farà contenere non solo il tuo codice ma migliaia e migliaia di righe di codice di dipendenza), o semplicemente memorizzare l'elenco dei pacchetti (ma ancora, se il manutentore della dipendenza apporta una modifica incompatibile con le versioni precedenti, tutto andrà in crash).
Come può essere migliorato?
Penso che l'esempio perfetto qui sia Node.js (e JavaScript in generale, suppongo) e NPM. NPM è un repository di pacchetti. Memorizza le diverse versioni dei pacchetti, quindi se hai bisogno di una versione specifica di un pacchetto, nessun problema: puoi ottenerla da lì. Inoltre, una delle cose in qualsiasi applicazione Node.js/JavaScript è il file package.json . Qui sono elencate tutte le dipendenze e le loro versioni, quindi puoi installarle tutte (e ottenere le versioni che funzionano sicuramente con il tuo codice) con npm install .
Inoltre, i grandi esempi di gestione dei pacchetti sono RubyGems/Bundler (per i pacchetti Ruby) e Crates.io/Cargo (per le librerie Rust).
4. Gestione degli errori
La gestione degli errori in Go è semplicissima. In Go, fondamentalmente puoi restituire più valori dalle funzioni e la funzione può restituire un errore. Qualcosa come questo:
err, value := someFunction(); if err != nil { // handle it somehow }Ora immagina di dover scrivere una funzione che esegue tre azioni che restituiscono un errore. Sembrerà qualcosa del genere:
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; }C'è molto codice ripetibile qui, il che non va bene. E con grandi funzioni, può essere anche peggio! Probabilmente avrai bisogno di un tasto sulla tastiera per questo:
Come può essere migliorato?
Mi piace l'approccio di JavaScript su questo. La funzione può generare un errore e puoi catturarlo. Considera l'esempio:
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 }È molto più chiaro e non contiene codice ripetibile per la gestione degli errori.
Le cose buone in Go
Sebbene Go abbia molti difetti di progettazione, ha anche alcune funzionalità davvero interessanti.
1. Goroutine
La programmazione asincrona è stata resa davvero semplice in Go. Mentre la programmazione multithreading è solitamente difficile in altri linguaggi, generare un nuovo thread ed eseguire una funzione in esso in modo che non blocchi il thread corrente è davvero semplice:
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }2. Strumenti inclusi in Go
Mentre in altri linguaggi di programmazione è necessario installare diverse librerie/strumenti per attività diverse (come test, formattazione di codice statico ecc.), Ci sono molti strumenti interessanti che sono già inclusi in Go per impostazione predefinita, come ad esempio:
-
gofmt- Uno strumento per l'analisi del codice statico. Rispetto a JavaScript, dove è necessario installare una dipendenza aggiuntiva, comeeslintojshint, qui è inclusa per impostazione predefinita. E il programma non verrà nemmeno compilato se non si scrive codice in stile Go (non si utilizzano variabili dichiarate, si importano pacchetti inutilizzati, ecc.). -
go test- Un framework di test. Ancora una volta, rispetto a JavaScript, è necessario installare dipendenze aggiuntive per il test (Jest, Mocha, AVA, ecc.). Qui è incluso per impostazione predefinita. E ti consente di fare molte cose interessanti per impostazione predefinita, come benchmarking, conversione del codice nella documentazione in test, ecc. -
godoc- Uno strumento di documentazione. È bello averlo incluso negli strumenti predefiniti. - Il compilatore stesso. È incredibilmente veloce, rispetto ad altri linguaggi compilati!
3. Rinviare
Penso che questa sia una delle caratteristiche più belle della lingua. Immagina di dover scrivere una funzione che apra tre file. E se qualcosa fallisce, dovrai chiudere i file aperti esistenti. Se ci sono molte costruzioni del genere, sembrerà un pasticcio. Considera questo esempio di pseudo-codice:
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; } Sembra complicato. È qui che entra in gioco il defer di 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 */ } Come vedi, se riceviamo un errore durante l'apertura del file numero tre, gli altri file verranno automaticamente chiusi, poiché le istruzioni di defer vengono eseguite prima di essere restituite in ordine inverso. Inoltre, è bello avere l'apertura e la chiusura di file nello stesso posto invece di parti diverse di una funzione.
Conclusione
Non ho menzionato tutte le cose buone e cattive in Go, solo quelle che considero le cose migliori e peggiori.
Go è davvero uno dei linguaggi di programmazione interessanti attualmente in uso e ha davvero del potenziale. Ci fornisce strumenti e funzionalità davvero interessanti. Tuttavia, ci sono molte cose che possono essere migliorate lì.
Se noi, come sviluppatori di Go, implementeremo queste modifiche, la nostra community sarà di grande beneficio, perché renderà la programmazione con Go molto più piacevole.
Nel frattempo, se stai cercando di migliorare i tuoi test con Go, prova a testare la tua app Go: inizia nel modo giusto del collega Toptaler Gabriel Aszalos.
