Tools

soldering

Controllo degli errori

Molte funzioni ritornano un valore che non è usato o un errore che spesso non è controllato.

Il compilatore rifiuta di compilare un programma in cui una variabile è dichiarata ma non usata, ma non ha alcun problema con la variabile finale di errore.

Per non andare incontro a errori futuri, il programmatore dovrebbe essere conscio di ogni funzione che ritorna un errore e, se sceglie di ignorarlo, dovrebbe farlo esplicitamente.

L'utility errcheck controlla che i valori ritornati siano gestiti o ignorati esplicitamente e opzionalmente tutti gli errori siano gestiti e non ignorati.

Installare con:

go get github.com/kisielk/errcheck

(831-errcheck.go):

package main

import "os"

func main() {
  f, _ := os.Open("foo")
  f.Write([]byte("Hello, world."))
  f.Close()
}

Dovrebbe scrivere "Hello, world." dentro il file foo. Compila, esegue, non scrive il file. Perchè? Passiamo alla fase di debugging.

Usiamo errcheck 83a-errcheck.go.

Tutte e tre le funzioni usate ritornano errori, ma lo menzioniamo esplicitamente e ignoriamo con la prima, non lo trattiamo con le altre due.

Notare inoltre che con l'aggiunta di un'opzione errcheck -blank 83a-errcheck.Gosegnala anche la prima funzione, cioè la menzione esplicita, ma il mancato trattamento, dell'errore.

Possiamo riscrivere il codice in modo che errcheck non si lamenti:

(832-errcheck.go):

package main

import "os"

func main() {
  f, _ := os.Open("foo")
  _, _ = f.Write([]byte("Hello, world.\n"))
  _ = f.Close()
}

Notare che errcheck -blank si lamenta lo stesso.

Lo scopo del Controllo Qualità non è di assicurarsi che il programma superi i controlli di qualità compiuti dai tools, ma di usare i tools per aumentare la qualità del programma.

Debugging ad alto livello

Riscriviamo il codice in modo che ogni errore venga usato:

(833-errcheck.go):

package main

import "os"

func main() {
  f, err := os.Open("foo")
  if err != nil {
    panic("Cannot open file")
  }
  _, err = f.Write([]byte("Hello, world.\n"))
  if err != nil {
    panic("Cannot write to file")
  }
  err = f.Close()
  if err != nil {
    panic("Cannot close file")
  }
}

Ora passa entrambe le varianti di errchecke quando lo lanciamo scopriamo dov'è che fallisce: non riesce ad aprire il file.
Qui sono i controlli errori che forzano i panic, se non lo facciamo, al programma va benissimo lo stesso: non compie le azioni richieste e prosegue.

Un panic significa che non si deve proseguire, perchè non si perseguirebbe lo scopo primario del programma.

OK, ma perchè non apre il file? E' giunto il momento di un sano RTFM sulla documentazione.

Soluzione

La funzione Open() apre un file che già esiste, e nel nostro caso tale file non c'è.
Se Open() fallisce per tale motivo, allora il file bisogna crearlo con Create():

(834-errcheck.go):

package main

import "fmt"
import "os"

func main() {
  f, err := os.Open("foo")
  if err != nil {
    f, err = os.Create("foo")
    if err != nil {
      panic("Cannot create file\n")
    }
  }
  fmt.Println("Open succeeded")
  _, err = f.Write([]byte("Hello, world.\n"))
  if err != nil {
    panic("Cannot write to file")
  }
  fmt.Println("Write succeeded")
  err = f.Close()
  if err != nil {
    panic("Cannot close file")
  }
  fmt.Println("Close succeeded")
}

Funziona. Passa errcheck in entrambe le versioni. Passa anche il vetting e il linting. Siamo contenti? No.

Provare, dopo la prima volta che si è eseguito, a dare il comando chmod -w foo e rieseguire il programma.

Evidentemente va in panic in fase di scrittura, e notiamo che non sa dirci perchè a questo livello, poteva essere l'assenza di permessi come in questo caso, ma poteva essere che il disco era pieno, o che mancava temporaneamente la rete in un accesso a disco remoto.

Quello che è importante è che non chiude il file.

Ora in questo caso il programma termina subito e disalloca tutte le risorse. Ma se vi fosse stato un loop, o eravamo in presenza di più goroutines, oppure si trattava di una connessione ad un database o a un socket?

Miglioramento della soluzione

Tutte le risorse devono essere garantite chiuse in caso di panic. Occorre usare defer.

(835-errcheck.go):

package main

import "fmt"
import "os"

func main() {
  f, err := os.Open("foo")
  if err != nil {
    f, err = os.Create("foo")
    if err != nil {
      panic("Cannot create file\n")
    }
  }
  fmt.Println("Open succeeded")
  defer func() {
    err = f.Close()
    if err != nil {
      panic("Cannot close file")
    }
    fmt.Println("Close succeeded")
  }()
  _, err = f.Write([]byte("Hello, world.\n"))
  if err != nil {
    panic("Cannot write to file")
  }
  fmt.Println("Write succeeded")
}

Da questo programma si nota che un fallimento della write, nel nostro caso per mancanza di permessi, causa comunque una chiusura del file.

Panic e Defer innestati

Alla domanda se si possano innestare dei defer e dei panic e se si possano cancellare i panic, risponde il seguente programma:

(836-multiple-panic):

package main

import "fmt"

func spoiler() {
  if e := recover(); e != nil {
    // e è il valore passato a panic
    fmt.Println("recovered from ", e)
  }
}

func later() {
  defer spoiler()
  panic("later panic")
}

func main() {
  fmt.Println("main about to panic")
  defer func() {
    panic("first defer panic")
  }()
  defer func() {
    defer later()
    panic("second defer panic")
  }()
  panic("main panic")
  fmt.Println("DON'T PANIC")
}

Dalla sua esecuzione si nota che:

  • una funzione defer può dichiarare un'altr funzione defer; all'esecuzione vengono sempre invocate in ordine inverso di registrazione
  • una funzione defer può invocare un panic anche se vi è già un altro panic attivo
  • vi possono essere molti panic attivi, ciascuno diverso
  • il recover di un panic non cancella gli altri
  • basta un solo panic ancora attivo e il programma esce
  • viene generato una stack trace per ciascun panic

Vetting

Il verbo inglese vet implica un controllo esaustivo e approfondito di qualcosa.

Go ha lo strumento go tool vet per tale tipo di controllo sul nostro codice.

Il seguenteè un programma che funziona, ma contiene molti errori che potrebbero rivelarsi fatali in condizioni future.

(837unvetted.go):

package main

import (
  "crypto/tls"
  "fmt"
  "sync"
  "sync/atomic"
  "unsafe"
)
// +build this,is,too,late

type T struct {
  A int `not a canonical tag`
}
// These fields should be keyed
_ = tls.Certificate{nil, nil, nil, nil, nil} 

// We meant to check foo() != nil
if foo != nil {

}
x := uintptr(0)
_ = unsafe.Pointer(x) // Misuse of unsafe.Pointer

for _, x := range []int{1, 2, 3} {
  go func() {
    fmt.Println(x) // Closing over xwrong
  }()
}

var b int32
b = atomic.AddInt32(&b, 1) // We shouldn't assign to b

var c bool
if c == false && c == true { // Always false
}

if c || c { // Redundant
}

fmt.Println("Got here")
return
fmt.Println("unreachable") // Unreachable code
}

Per verificarlo, lanciare

go tool vet 837unvetted.go

e notare la quantità di rapporti prodotti.

Linting

Lint sono in inglese le palle di polvere che si formano sul pavimento o sui vestiti. Linting è il toglierle.

Il programma lint è sempre esistito per gli sviluppatori in linguaggio C fino dagli anni '80. La versione moderna per Linux è splint.

In Go è disponibile golint come package contribuito.
Per installarlo:

go get github.com/golang/lint/golint

Per testarlo usiamo un programma:

(838unlinted.go):

package lint

import (
  "errors"
  . "fmt"
)
var SomeError = errors.New("Capitalised error message")
type unexported int
type Exported int
func (this unexported) Foo() {}
func (oneName Exported) Foo() {
}
func (anotherName Exported) Bar() {
  Println("Hi")
}

Verifichiamolo con:

golint 838unlinted.go