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