Ripasso

Settimana scorsa abbiamo parlato di gestione dei file. Fortunatamente, la gestione dei file è molto semplice e bisogna imparare soltanto 3 funzioni:

os.Open(name string) (*os.File, error)
os.Create(name string) (*os.File, error)
os.OpenFile(name string, flag int, perm FileMode) (*os.File, error)

e ricordarsi di chiudere il file una volta aperto.

Gestione degli errori

Oggi impariamo qualcosa che sarà molto utile in tutti i programmi che scriveremo da qui in avanti, e che ritornerà molto spesso anche nell'utilizzo della libreria standard di Go, o di librerie esterne. Si tratta della gestione degli errori.

Ogni funzione, come abbiamo visto, può avere più di un valore di ritorno e molto spesso le funzioni che abbiamo usati ritornano due valori, per esempio:

os.Open(name string) (*os.File, error)

ritorna due valori: uno di tipo *os.File e l'altro di tipo error. Come si può intuire il secondo valore sarà ciò su cui concentreremo la nostra attenzione oggi.

Error

È uso comune tra i programmatori go utilizzare una particolare struttura per gestire gli errori: ogni volta che la funzione che si sta sviluppando potrebbe incontrare un errore, questo viene catturato in una variabile di tipo error che viene poi restituita alla fine della funzione per essere gestita. Le variabili di tipo error possono essere stampate, il loro contenuto sarà il messaggio di errore, per esempio se cercate di aprire un file inesistente:

f, err := os.Open("inesistente.txt") // err conterrà l'errore
// se non c'è nessun errore il valore di err sarà nil
if err != nil { // significa che c'è stato qualche errore
	fmt.Fprintln(os.Stderr, err) // usiamo lo standard error!
	return // blocchiamo l'esecuzione del programma
}

Il risultato di questo programma sarà:

open inesistente.txt: no such file or directory

Creare un error

Per utilizzare questo idioma nelle nostre funzioni dovremo usare la funzione

Errorf(format string, a ...interface{}) error

che funziona come fmt.Printf, cioè accetta una stringa che specifica come formattare l'errore, ma invece di stampare il risultato a video lo ritorna con tipo errore. Ad esempio una funzione che non fa nulla ma ritorna un errore:

func alwaysError() error {
	var err error
	err = fmt.Errorf("So solo sbagliare\n")
	return err
}

oppure

func alwaysError() error {
	return fmt.Errorf("So solo sbagliare\n")
}

Gestire un error

Quando vi ritrovate a lavorare con funzioni che ritornano degli errori (vedi funzioni della famiglia di fmt.Scan), potete decidere cosa fare nel momento in cui incontrate un errore.

Solitamente si distingue tra due tipi di errori: errori dell'utente ed errori del programmatore. Una buona regola è cercare di risolvere gli errori dell'utente e bloccare l'esecuzione del programma nel caso di errori del programmatore. Per esempio, se l'utente inserisce il nome di un file che non esiste si può decidere di ignorare quel file e proseguire notificando l'utente.

var filename string
fmt.Print("File: ")
fmt.Scan(&filename)
f, err := os.Open(filename)
if err != nil {
	fmt.Fprintf(os.Stderr, "Il file %s non esiste!\n", filename)
}
// proseguo comunque

Tipi di errore

error permette anche di specificare il tipo di errore che si vuole ritornare. Per esempio, il pacchetto io definisce il tipo di errore io.EOF che è quello che ritornano le funzioni tipo fmt.Scan quando raggiungono la fine di un file, questo era utile per gli esercizi della scorsa volta:

err := fmt.Fscanf("%c", &c)
if err == io.EOF {
	// Il file è finito
}

Funzioni che ritornano bool

Un altro modo di gestire gli errori è quello di restituire un valore booleano al posto di un error. Solitamente questo valore booleano viene interpretato come un ok, ovvero: se il valore è true significa che non ci sono stati errori, altrimenti qualcosa è andato storto. Per esempio:

func main() {
	s, ok := neverOk()
	if ! ok {
		fmt.Fprintf("Qualcosa è andato storto: %s\n", s)
	}
	return
}
	
func neverOk() (string, bool) {
	return "Non è tutto ok", false
}

Questo tipo di gestione degli errori sta progressivamente sparendo vista la maggiore flessibilità del tipo error (che per esempio permette di descrivere quale sia stato l'errore e di specificare diversi tipi di errore).

Array

Dato che la gestione degli errori è un argomento interessante ma poco applicativo, occorre aggiungere qualcosa per poter fare esercizi oggi. Iniziamo quindi a parlare di collezioni, ovvero strutture che contengono un certo numero di elementi. Molti di questi sono comuni ad altri linguaggi di programmazione.

Il primo tipo di struttura che trattiamo sono gli array: contenitori di un numero fissato di elementi dello stesso tipo. Gli array sono indicati con la notazione [n] dove n è il numero di elementi. Per esempio, l'esercizio della media di 10 numeri potrebbe essere risolto con un array di 10 float64:

var voti [10]float64

la variabile voti rappresenta un contenitore per 10 float64. Questo tipo di collezione è poco flessibile:

Accesso agli elementi

Per accedere alle variabili che compongono un array si usa l'operazione di indicizzazione: ogni elemento dell'array occupa una posizione che varia tra 0 e la lunghezza - 1. La sintassi è la seguente:

var voti [10]float64
voti[0] = 5 // Il primo voto sarà 5

Se si cerca di accedere ad un elemento che non è nel range corretto (>= len(array)) il programma va in crash (il compilatore non se ne accorge).

Listare elementi di un array

Per accedere a tutti gli elementi di un array si usano cicli for, di due tipi: semplici cicli for

var array1 [5]int
for i := 0; i < len(array1); i++ {
	fmt.Println(array1[i])
}

e cicli for range:

var array2 [55]int

for i := range array2 { // i è un intero contenente l'indice
	fmt.Println(array2[i])
}

questi costrutti oltre che listare gli elementi permettono anche di cambiarli: gli array sono strutture read-write.

var array3 [3]int

for i := 0; i < len(array3); i++ {
	array3[i] = i + 1
}

array3 conterrà i numeri 1, 2, 3.

Passare gli array come argomenti di funzioni

Come detto sopra, la lunghezza dell'array ne cambia il tipo, quindi nella dichiarazione di una funzione che ha come input un array dovete specificarla. Per esempio la funzione che calcola la media di 10 voti contenuti in un array può essere dichiarata così:

func media(voti [10]float64) float64

Esercizi

Esercizio 0 - Media di 10 voti

Problema: Implementare il solito problema della media dei voti utilizzando un array di tipo [10]float64. Leggete i voti da un file voti.txt che contenga 10 numeri.

Soluzione

package main

import (
	"fmt"
	"os"
)

func main() {
	voti, err := leggiVoti("voti.txt")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1) // Qualunque numero diverso da zero indica un errore
	}

	fmt.Println("Media: ", media(voti))
	return
}

// Legge un array di 10 voti dal file filename
func leggiVoti(filename string) ([10]float64, error) {
	var voti [10]float64
	f, err := os.Open(filename)
	if err != nil {
		return voti, err
	}

	for i := 0; i < 10; i++ {
		_, err = fmt.Fscan(f, &voti[i])
		if err != nil {
			return voti, err
		}
	}

	f.Close()
	return voti, nil
}

// Restituisce la media dei voti
func media(voti [10]float64) float64 {
	var sum float64

	for i := range voti {
		if voti[i] < 3 {
			fmt.Fprintf(os.Stderr, "Voto non valido %f, utilizzo il valore valido più vicino: %d\n", voti[i], 3)
			sum += 3
		} else if voti[i] > 10 {
			fmt.Fprintf(os.Stderr, "Voto non valido %f, utilizzo il valore valido più vicino: %d\n", voti[i], 10)
			sum += 10
		} else {
			sum += voti[i]
		}
	}

	return sum / 10
}

Esercizio 1 - Database 3

Problema: Scrivere un programma database.go che legge da un file database.txt che abbia questa sintassi:

materia voto

e calcola la media dei voti associati ad una particolare materia. Le materie possibili sono 3: matematica, fisica e programmazione, a ciascuna di queste associate un array di 10 float64 che rappresentano i voti. Stampate le medie associate a ciascuna delle 3 materie. Ricordatevi di controllare se i voti sono nel range corretto e gestite l'errore se non lo sono. Spezzate il codice in funzioni per aiutarvi a mantenerlo ordinato.

Nota: Penso che valga la pena provare a fare bene almeno questo esercizio.

Esempio di esecuzione:

medie:
matematica 5.5
fisica 6
programmazione 10

il file database.txt:

matematica 4
matematica 7
fisica 3
programmazione 10
fisica 6
matematica 5.5
... 10 voti per ogni entrata

Soluzione

package main

import (
	"fmt"
	"os"
)

var matematica, fisica, informatica [10]float64

func main() {
	err := leggiVoti("database.txt")
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	fmt.Println("Medie:")
	fmt.Println("Matematica: ", media(matematica))
	fmt.Println("Fisica: ", media(fisica))
	fmt.Println("Informatica: ", media(informatica))

	return
}

func leggiVoti(filename string) error {
	var materia string
	var matIndex, fisIndex, infIndex int

	f, err := os.Open(filename)
	if err != nil {
		return err
	}

	for matIndex <= 9 || infIndex <= 9 || fisIndex <= 9 {
		var voto float64
		if _, err = fmt.Fscanf(f, "%s %f\n", &materia, &voto); err != nil {
			return err
		}

		switch materia {
		case "matematica":
			if matIndex < 10 {
				matematica[matIndex] = voto
			} else {
				fmt.Fprintf(os.Stderr, "Numero massimo di voti di %s raggiunto, ignoro %f\n", materia, voto)
			}
			matIndex++
		case "fisica":
			if fisIndex < 10 {
				fisica[fisIndex] = voto
			} else {
				fmt.Fprintf(os.Stderr, "Numero massimo di voti di %s raggiunto, ignoro %f\n", materia, voto)
			}
			fisIndex++
		case "informatica":
			if infIndex < 10 {
				informatica[infIndex] = voto
			} else {
				fmt.Fprintf(os.Stderr, "Numero massimo di voti di %s raggiunto, ignoro %f\n", materia, voto)
			}
			infIndex++
		}
	}

	f.Close()
	return nil
}

// Restituisce la media dei voti
func media(voti [10]float64) float64 {
	var sum float64

	for i := range voti {
		if voti[i] < 3 {
			fmt.Fprintf(os.Stderr, "Voto non valido %f, utilizzo il valore valido più vicino: %d\n", voti[i], 3)
			sum += 3
		} else if voti[i] > 10 {
			fmt.Fprintf(os.Stderr, "Voto non valido %f, utilizzo il valore valido più vicino: %d\n", voti[i], 10)
			sum += 10
		} else {
			sum += voti[i]
		}
	}

	return sum / 10
}

Esercizio 2 - Fibonacci

Problema: Scrivere un programma go fib.go che calcola i primi 10 numeri della sequenza di fibonacci. Utilizzate un array di 10 interi e ricordatevi che vi servono sempre 2 interi della sequenza per calcolare quello successivo.

Definizione: I primi due numeri della sequenza di fibonacci sono 0 e 1, tutti gli altri numeri sono la somma dei due precedenti nella sequenza.

Soluzione

package main

import (
	"fmt"
)

func main() {
	fmt.Println(fib())
	return
}

func fib() [10]int {
	var fibonacci [10]int

	fibonacci[0] = 0
	fibonacci[1] = 1

	for j := 1; j < 9; j++ {
		fibonacci[j + 1] = fibonacci[j] + fibonacci[j - 1]
	}
	return fibonacci
}