Обработване на грешки

24.11.2015

В този епизод:

Въпрос #1:

За какво често се полва sync.WaitGroup?

Въпрос #2:

За какво бихте използвали sync.RWMutex?

Въпрос #3:

Какво ще направи select ако никой не пише в каналите му в даден момент?

Въпрос #4:

Какво би направил следния код:

package main

import "fmt"
import "time"

func main() {
    c := make(chan int)

    go func() {
        c <- 42
    }()

    select {
    case v1 := <-c:
        fmt.Printf("received %d in v1\n", v1)
    case v2 := <-c:
        fmt.Printf("received %d in v2\n", v2)
    case <-time.After(1 * time.Nanosecond):
        fmt.Printf("timeout\n")
    default:
        fmt.Printf("nothing!\n")
    }
}

Who knows :)

Въпрос #5:

Как се правят generators/iterators/lazy loading в Go?

Error handling

Имало едно време чисто С

Пример в C

#include <stdio.h>
#include <errno.h>
#include <string.h>

extern int errno;

int main ()
{
    FILE* pf = fopen("unexist.txt", "rb");
    if (pf == NULL)
    {
        fprintf(stderr, "Value of errno: %d\n", errno);
        perror("Error printed by perror");
        fprintf(stderr, "Error opening file: %s\n", strerror(errno));
    }
    else
    {
        fclose(pf);
    }
    return 0;
}

Имало едно време един език Go

Има грубо-казано 2 начина

Връщане на грешка

type error interface {
    Error() string
}
type PathError struct {
    Op string    // "open", "unlink", etc.
    Path string  // Файлът с грешката
    Err error    // Грешката, върната от system call-a
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

Стандартна употреба

func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    //...
}

или малко по-сложно:

func CreateFile(filename string) (*os.File, error) {
    var file, err = os.Create(filename)
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles() // Free some space
        return os.Create(filename)
    }
    return file, err
}

Errors are values

Често оплакване на Go програмисти е количеството проверки за грешки:

if err != nil {
    return err
}

Пример

if _, err := fd.Write(p0[a:b]); err != nil {
    return err
}
if _, err := fd.Write(p1[c:d]); err != nil {
    return err
}
if _, err := fd.Write(p2[e:f]); err != nil {
    return err
}

Може да стане:

var err error
write := func(buf []byte) {
    if err == nil {
        _, err = w.Write(buf)
    }
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
if err != nil {
    return err
}

defer

Пример:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

Какви са проблемите с този код?

По-красивият, правилен и работещ начин е това:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

Доуточнения

Три прости правила за defer (1)

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

Три прости правила за defer (2)

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

Три прости правила за defer (3)

func c() (i int) {
    defer func() { i++ }()
    return 1
}

Примери

package main

import (
	"fmt"
)

func deferExample() {
    for i := 0; i < 5; i++ {
        defer func(i int) {
            fmt.Printf(" %v", i)
        }(i)
    }
}

func main() {
	deferExample()
}

-

package main

import (
	"fmt"
)

func deferExample() {
    for i := 0; i < 5; i++ {
        defer func() {
            fmt.Printf(" %v", i)
        }()
    }
}

func main() {
	deferExample()
}

Паника!

Уточнения

Избягвайте ненужното изпадане в паника

recover

Example

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}
func g(i int) {
    if i > 2 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}
package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in f", r)
		}
	}()
	fmt.Println("Calling g.")
	g(0)
	fmt.Println("Returned normally from g.")
}
func g(i int) {
	if i > 2 {
		fmt.Println("Panicking!")
		panic(fmt.Sprintf("%v", i))
	}
	defer fmt.Println("Defer in g", i)
	fmt.Println("Printing in g", i)
	g(i + 1)
}

//END OMIT

Тестове и документация

Disclamer

Днес няма да си говорим за acceptance testing, quality assurance или нещо, което се прави от "по-низшия" отдел във фирмата.

Всичко тук е дело на програмиста.

Митът

Проектът идва с готово, подробно задание.

Прави се дизайн.

С него работата се разбива на малки задачи.

Те се извършват последователно.

За всяка от тях пишете кода и приключвате.

Изискванията не се променят, нито се добавя нова функционалност.

Митът v2.0

Щом съм написал един код, значи ми остава единствено да го разцъкам - няколко print-а, малко пробване в main функцията и толкова.

Така или иначе няма да се променя.

А ако (не дай си боже) това се случи - аз съм го писал, знам го, няма как да допусна грешка.

Най-много да го поразцъкам още малко.

Тежката действителност

Заданията винаги се променят.

Често се налага един код да се преработва.

Писането на код е сложна задача - допускат се грешки.

Програмистите са хора - допускат грешки.

Промяната на модул в единия край на системата като нищо може да счупи модул в другия край на системата.

Идва по-добра идея за реализация на кода, по ред причини.

Искаме да автоматизираме нещата

За всичко съмнително ще пишем сценарий, който да "цъка".

Всеки сценарий ще изпълнява кода и ще прави няколко твърдения за резултатите.

Сценариите ще бъдат обединени в групи.

Пускате всички тестове с едно бутонче.

Резултатът е "Всичко мина успешно" или "Твърдения X, Y и Z в сценарии A, B и C се оказаха неверни".

Искаме да тестваме и производителността на нашия код.

Видове тестове

За какво ни помагат тестовете

За какво не служат тестовете

testing

Разбрахме се, че тестовете са ни супер важни.

Очевидно в стандартната библиотека на Go, има пакет за това.

За да тестваме foo.go, създаваме foo_test.go в същата директория, който тества foo.go

Ако тестваме пакета foo можем:

Или да ги смесваме.

Тестовете в `testing`

func TestFibonacciFastest(t *testing.T) {
    n := FibonacciFastest(0)
    if n != 1 {
        t.Error("FibonnaciFastest(0) returned" + n + ", we expected 1")
    }
}

Benchmark тестове

func BenchmarkFibonacciFastest(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibonacciFastest(40)
    }
}

Demo

Документиране на кода

go генерира автоматична документация на нашия код, вземайки под внимание:

/*
    Example fobonacci numbers implementation
*/
package fibonacci
// Fastest Fibonacci Implementation
func FibonacciFastest(n uint64) uint64 {
// lookupTable stores computed results from FibonacciFast or FibonacciFastest.
var lookupTable = map[uint64]uint64{}

Виждане на документацията

На всички локално инсталирани пакети

godoc -http=:6060

Документация на (почти) всички go пакети качени в BitBucket, GitHub, Launchpad и Google Project Hosting

Example тестове - шантавата част

Foo -> ExampleFoo
func ExampleHello() {
    Hello("hello")
    // Output:
    // hello
}

Въпроси?