Una piccola lista di principali errori che si riscontrano maggiormente nei principianti le prime volte che usano GO.
I bug nel codice
I bug nel codice sono problemi che possono causare errori nell’esecuzione del software e interruzioni in produzione. Un bug è un difetto nel codice che produce risultati indesiderati o errati. Il codice sorgente di un programma presenta spesso bug a causa di pratiche di scrittura del codice inadeguate, tempi per lo sviluppo dell’applicazione troppo brevi o errori presenti nei tools di terze parti. In questo post esamineremo alcuni dei più comuni rischi di bug in Go.
Chiamata ricorsiva infinita
Una funzione che chiama se stessa in modo ricorsivo deve avere una condizione di uscita. Altrimenti, la funzione richiamerà se stessa fino a quando il sistema non esaurirà la memoria.
Questo problema può essere causato da errori comuni come dimenticare di aggiungere una condizione di uscita. Può anche accadere “intenzionalmente”. Alcuni linguaggi di programmazione hanno l’ottimizzazione delle chiamate di coda (tail-call), che la rende sicura da usare in certe chiamate ricorsive infinite. L’ottimizzazione delle tail-call consente di evitare di allocare un nuovo stack-frame per una funzione perché la funzione chiamante restituirà il valore che ottiene dalla funzione chiamata. Go, tuttavia, non implementa le tail-call e alla fine, se non gestito correttamente, si esaurirà la memoria nell’elaboratore. Questo problema, però, è escluso e non si applica nella generazione di nuove goroutine.
Lettura consigliata: Why is a Goroutine’s stack infinite ?
Assegnazione di nil
a map
map
deve essere inizializzata utilizzando la funzione make
(o map
) prima di poter aggiungere qualsiasi elemento. Viene creato un nuovo valore
di map
vuoto utilizzando la funzione incorporata make
, che accetta come argomenti il tipo di map ed eventualmente la sua capacità:
make (mappa [string] int)
make (mappa [string] int, 10)
La capacità iniziale non ne limita le dimensioni: le map
aumentano le loro dimensioni e crescono per accogliere il numero di elementi in esse immagazzinato,
ad eccezione di quelle nulle. Una map
nil è equivalente a una mappa vuota tranne per il fatto che non possono essere aggiunti elementi.
Inizializzazione errata:
var datiMap map [string] [] MapElemento
Inizializzazione corretta:
datiMap: = make (map [string] [] MapElemento)
Lettura consigliata: Go: assignment to entry in nil map
Modifica del ricevitore
Un metodo che modifica un valore del ricevitore non puntatore può avere conseguenze indesiderate. Questo è un caso in cui aumenta il rischio di bug perché il metodo può modificare il valore del ricevitore all’interno del metodo, ma non si rifletterà nel valore originale. Per propagare la modifica, il destinatario deve essere un puntatore.
Per esempio:
package main
import (
"fmt"
)
type data struct {
num int
}
func (d data) setNum() {
d.num = 5
}
func (d data) run() {
d.vmethod()
fmt.Printf("%+v", d) // Output: {num:2}
}
func main() {
fmt.Println("Starting")
var d data
d.setNum = 2
d.run()
}
Invece num
deve essere modificato nel seguente modo:
package main
import (
"fmt"
)
type data struct {
num int
}
func (d *data) setNum() { // --> Pointer
d.num = 5
}
func (d data) run() {
d.setNum()
fmt.Printf("%+v", d) // Output: {num:5}
}
func main() {
fmt.Println("Starting")
var d data
d.num = 2
d.run()
}
Possibili valori indesiderati usati nelle goroutine
Le variabili temporanee in cicli vengono riutilizzate ad ogni iterazione; pertanto, una goroutine creata in un ciclo punterà alla variabile di intervallo dall’ambito superiore. In questo modo, la goroutine potrebbe utilizzare la variabile con un valore indesiderato.
Nell’esempio seguente, il valore di index e il valore utilizzato nella goroutine provengono dall’ambito esterno. Poiché le goroutine vengono eseguite in modo asincrono, il valore di index e value potrebbe essere (e di solito lo sono) diverso dal valore previsto.
valori := []string{"A", "B", "C"}
for index, value := range valori {
go func() {
fmt.Printf("Index: %d\n", index)
fmt.Printf("Value: %s\n", value)
}()
}
Per ovviare a questo problema, una variabile di scopo viene creata, come nell’esempio che segue:
valori := []string{"A", "B", "C"}
for index, value := range valori {
index := index
value := value
go func() {
fmt.Printf("Index: %d\n", index)
fmt.Printf("Value: %s\n", value)
}()
}
Un altro modo di affrontare il problema è quello di passare il valore della variabile alla goroutine come nel seguente caso:
valori := []string{"A", "B", "C"}
for index, value := range valori {
go func(index int, value string) {
fmt.Printf("Index: %d\n", index)
fmt.Printf("Value: %s\n", value)
}(index, value)
}
Lettura consigliata: What happens with closures running as goroutines?
Il deferimento di Close
prima di controllare eventuali errori
È abitudine nel linguaggio Go di usare il comando Close()
per un valore come nelle interfacce di io.Closer
. Per esempio, quando si apre un file:
f, err := os.Open("/tmp/file.txt")
if err != nil {
return err
}
defer f.Close()
Ma questo codice è dannoso per i file scrivibili perché il rinvio della funzione (defer f.Close()
) ignora il suo valore di ritorno e il metodo Close()
può restituire a sua
volta errori. Ad esempio, se sono stai scritti dei dati nel file, potrebbero essere stati memorizzati nella cache e non scaricati, ancora, nel disco al
momento in cui viene chiamata la funzione Close()
. Questo errore dovrebbe essere gestito in modo esplicito.
Sebbene non è obbligatorio utilizzare defer
, risulta scomodo ricordarsi di chiudere il file ogni volta che il lavoro sul file è stato completato. Un modo
migliore sarebbe quello di rinviare a una funzione wrapper, come nell’esempio seguente.
f, err := os.Open("/tmp/file.txt")
if err != nil {
return err
}
defer func() {
chiusoErr := f.Close()
if chiusoErr != nil {
if err == nil {
err = chiusoErr
} else {
log.Println("Errore nella chiusura del file :", chiusoErr)
}
}
}()
return err
Lettura consigliata: Do not defer Close() on writable files