poniedziałek, 19 marca 2012

Gopher test

W standardowej bibliotece Go nie ma klasycznej wersji Unit Testów. Nie załamujcie się przeto wyznawcy TDD, nie popadajcie w marazm, niech nie pochłonie was rozpacz, albowiem istnieje światełko nadziei dla Was. Wielki i wszechmocny Gopher Was nie opuści.

Daje on narzędzia do prostego testowania i uruchamiania tychże testów. Są proste, a jednocześnie dość efektowne, jak cały język.

W niniejszym wpisie postaram się wyłożyć prosty przykład Gophowego pakietu wraz z klasą i przykładami testów.

Po pierwsze, tworzymy pakiet strdict, a w nim tworzymy Gopherowy typ Dict wraz z jego metodami.
Wszystko to zapisujemy w pliki strdict.go w katalogu GOPATH/src/strdict.

--------------
package strdict

import (
    "fmt"
)
// new type
type Dict map[string]string

// get value with key
// if key not present then raise panic
func(m Dict) Get(key string) string {
    k, ok := m[key]
    if !ok{
        panic(fmt.Sprintf("No key '%s' in Dict", key))
    }
    return k
}
// get value with key
// if key not present then return def
func(m Dict) GetDef(key, def string) string {
    k, ok := m[key]
    if !ok{
        return def
    }
    return k
}

// check if m has key value
func (m Dict) HasKey(key string) bool{
    _, ok := m[key]
    return ok
}
// set value with key
func(m Dict) Set(key, value string)  {
    m[key] = value
} 
// print all keys and values 
func (m Dict) Print(){
    for k, v := range(m){
        fmt.Println(k, "  ", v)
    }
}

--------------

Teraz tworzymy plik z testami do pakietu. Nazywamy go strdict_test.go i zapisujemy w tym samym katalogu co strdict.go.

--------------
package strdict

import (
    "testing"
    "fmt"
)

func TestSet(t *testing.T){
    m1 := Dict{}
    m2 := Dict{}
    m1.Set("a", "a")
    m1.Set("a", "b")
    m1.Set("a", "v")
    m1.Set("d", "f")
    m2.Set("a", "zzz")
    if m1["a"] != "v"{
        t.Error("Set error. No value in map")
    }
}

func TestGet(t *testing.T){
    m := make(Dict)
    m["a"] = "b"
    m["b"] = "c"
    m["c"] = "d"
    if m.Get("a") != "b"{
        t.Error("Get error. Wrong value for key ")
    }
    defer func(){
        if err := recover(); err == nil{
            t.Error("Get error. Getting non existing value should raise panic")
        }
    }()
    m.Get("non existing key")
}

func TestHasKey(t *testing.T){
    m := make(Dict)
    m["a"] = "a"
    m["b"] = "b"
    if !m.HasKey("a"){
        t.Error("Error checking existing key")
    }
    if m.HasKey("c"){
        t.Error("Error checking none existing key")
    }
}
func ExampleGet(){
    m := Dict{}
    m["a"] = "123"
    fmt.Println(m.Get("a"))
    //Output:
    // 123
}

func BenchmarkGet(b *testing.B){
    b.StopTimer()
    m := Dict{}
    m["aa"] = "aa"
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        _ =m.Get("aa")
    }
}

func BenchmarkSet(b *testing.B){
    b.StopTimer()
    m := Dict{}
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        m.Set("aa", "1")
    }
}
    

--------------

Parę słów objaśniających testy:

TestSet, TestGet, TestHasKey - klasyczne przykłady testów jednostkowych. Ich nazwy muszą rozpoczynać się od Test... 
W tym jednak wypadku nie mamy instrukcji assert, lecz używamy instrukcji if i t.Error(). 

ExampleGet - ta funkcja testująca porównuje wyjście standardowe, czyli w tym przypadku wynik instrukcji fmt.Println ze spodziewanym wyjściem które umieściliśmy w komentarzu. Jeśli będą różne wówczas testy zakończą się niepowodzeniem. Musi się zaczynać od Example.

Innymi ciekawymi przykładami są funkcje BenchmarkSet i BenchmarkGet. Mierzą one średni czas wykonania operacji.



Sposób wywołania testów w konsoli:

----------------------------------------------------------------------------
user@user-home:~$ go test strdict
ok   strdict 0.006s
----------------------------------------------------------------------------

Sposób wywołania testów wraz z benchmarkami:

----------------------------------------------------------------------------
go test -test.bench="." strdict
PASS
BenchmarkGet 20000000        109 ns/op
BenchmarkSet 20000000        114 ns/op
ok   strdict 4.718s
----------------------------------------------------------------------------

środa, 7 marca 2012

Pierwszy konkretny przykład

W ramach nauki postanowiłem napisać aplikację, która korzystając z charakterystycznego dla go przetwarzania równoległego będzie przeszukiwać foldery w poszukiwaniu plików z nazwami pasującymi do wyrażenia regularnego.

Pierwsze założenie było takie, żeby każdy folder przetwarzany był w osobnej gorutynie i zwracał wyniki kanałem, bądź porostu wypisywał je na standardowym wyjściu. 

Program działał całkiem fajnie i wydajnie przy małej ilości plików do przeszukania. Kiedy natomiast uruchomiłem go dla własnego katalogu HOME, lub dysku C na windowsie, po chwili produkował sporą ilość błędów wynikającą ze zbyt dużej liczby uchwytów do plików. Okazało się ze otwieranie i przetwarzanie katalogu w osobnym procesie powodowało lawinowy przyrost otwartych plików, z którymi zarówno Ubuntu jak i Windows7 nie były sobie w stanie poradzić. 

Wróciłem więc i przeprojektowałem pogram w ten sposób, aby pliku otwierane były tylko w głównej gorutynie, natomiast proces dopasowywania nazw plików do wyrażenia regularnego odbywał się w osobnych procesach.

Zaczynając od początku. Tworzymy strukturę będzie przechowywać konfigurację naszego przeszukiwania, czyli katalog od którego startujemy, wyrażenie którego szukamy. Struktura w Go pełni rolę swego rodzaju obiektu. Posiada zaalokowaną pamięć oraz swoje atrybuty. Przypomnieć można, że jedynie atrybuty, które zaczynają się wielgachną literą będą widoczne poza strukturą, czyli można powiedzieć, publiczne.

------------------------------
type Config struct {
    StartPath            string
    SearchNamePattern    *regexp.Regexp
    SearchContentPattern *regexp.Regexp
}
------------------------------

Następnie przygotowujemy funkcje konstruującą.  W Go nie istnieją znane z innych języków konstruktory, dlatego często tworzy się funkcje, które inicjują i zwracają wskaźnik do nowego obszaru pamięci zarezerwowanej przez strukturę. Nazwy w Go powinny być jednoznaczne, możliwie krótkie i oczywiście znaczące. Wszystko w myśl zasady KISS you FOOL.
W naszym przypadku funkcja taka mogła by mieć postać:

------------------------------
func NewConfig(startDir string, snp string) Config {
    snpRe := regexp.MustCompile(snp)
    return Config{startDir, snpRe}
}
------------------------------
Funkcja zaczyna się wielgachną literą więc będzie widoczna na zewnątrz modułu. Skoro jednak nasza struktura również zaczyna się od dużej litery, alternatywnym sposobem utworzenia naszego konfiga byłoby: return Config{startDir, snpRe}.

Przygotujmy teraz funkcje odczytujące zawartość katalogu i przetwarzające listę plików z niego. Prawdziwe wąskie gardło naszej aplikacji.
------------------------------
//
// Process directory given as a path
//
func processDir(p string) []os.FileInfo {
    var (
        f   *os.File
        fi  []os.FileInfo
        err error
    )

    if f, err = os.Open(p); err != nil {
        return fi
    }
    defer f.Close()
    if fi, err = f.Readdir(-1); err != nil {
        return fi
    }
    return fi
}

//
// Proces list of file infos to search re in files
//
func processList(basePath string, list []os.FileInfo, re *regexp.Regexp, namesCh, dirCh, processedCh chan string) {
    for _, fileInfo := range list {
        if fileInfo.IsDir() {
            dirCh <- path.Join(basePath, fileInfo.Name())
        }
        if re.MatchString(fileInfo.Name()) {
            namesCh <- path.Join(basePath, fileInfo.Name())
        }
    }
    processedCh <- basePath
}
------------------------------

I tu zaczynamy używać kanałów.
Kanał namesCh przekazuje informacje o plikach których nazwy spełniają kryterium.
Do kanału dirCh zapisujemy ścieżkę do katalogu jeśli takowy znajdziemy.
Katalog processedCh informuje nas, że skończyliśmy przetwarzać katalog o nazwie basePath.

Poniżej zaś implementacja głównej procedury. Ta wersja gromadzi listę plików w splice. Zwraca pełną listę wyników.
Do monitorowania, które katalogi zostały już przetworzone służy mapa balancer. Kiedy  będzie ona pusta oznacza to, że nie ma więcej katalogów do przetworzenia.
------------------------------
func process(conf Config, namesCh, dirCh, processedCh chan string) []string {
    startPath := conf.StartPath
    re := conf.SearchNamePattern
    fi := processDir(startPath)
    balancer := make(map[string]bool, 10)
    var fileList []string
    go processList(startPath, fi, re, namesCh, dirCh, processedCh)
    balancer[startPath] = true
    for {
        if len(balancer) <= 0 {
            break
        }
        select {
        case dir := <-dirCh:
            fi := processDir(dir)
            go processList(dir, fi, re, namesCh, dirCh, processedCh)
            balancer[dir] = true
        case dir := <-processedCh:
            delete(balancer, dir)
        case fpath := <-namesCh:
            fileList = append(fileList, fpath)
        }
    }
    close(namesCh)
    return fileList
}
------------------------------

Dodajmy jeszcze główną funkcję, która będzie wyeksportowana w pakiecie.
------------------------------
//
// Func returns list of files which maches to patterns from conf
//
func Find(conf Config) []string {
    // Chanel to recive names of matching files
    var namesCh = make(chan string)
    // Chanel to recive names of dir names 
    var dirCh = make(chan string)
    // chanel to recive finished dirs
    var processedCh = make(chan string)
    files := process(conf, namesCh, dirCh, processedCh)
    return files
}
------------------------------

A teraz pomyślmy dlaczego nasz program który korzysta z naszej biblioteki musi czekać aż skończymy przetwarzać wszystkie pliki? Czemu nie może przekazać do funkcji kanału i pobierać z niego pasujące pliki kiedy tylko zostaną one zapisane do kanału? Ależ oczywiście że może. Co pokazuje poniższa implementacja.

------------------------------
//
// Function gets channel as parameters. Sends to outCh files
// which matches to patters from conf
func GoFind(conf Config, outCh chan string) {
    // Chanel to recive names of dir names 
    var dirCh = make(chan string)
    // chanel to recive finished dirs
    var processedCh = make(chan string)

    goprocess(conf, outCh, dirCh, processedCh)

}

//
//
//
func goprocess(conf Config, namesCh, dirCh, processedCh chan string) {
    startPath := conf.StartPath
    re := conf.SearchNamePattern
    fi := processDir(startPath)
    balancer := make(map[string]bool, 10)
    go processList(startPath, fi, re, namesCh, dirCh, processedCh)
    balancer[startPath] = true
    for {
        if len(balancer) <= 0 {
            break
        }
        select {
        case dir := <-dirCh:
            fi := processDir(dir)
            go processList(dir, fi, re, namesCh, dirCh, processedCh)
            balancer[dir] = true
        case dir := <-processedCh:
            delete(balancer, dir)
        }
    }
    close(namesCh)
}
------------------------------

poniedziałek, 27 lutego 2012

Golang drugie starcie

Dlaczego od razu drugie?
Pierwsze było w 2009, kiedy ogłoszono wydanie Golang. Zafascynowany (zresztą do dziś) Pythonem, przeczytawszy, że oferuje kodowanie łatwe jak w językach z kaczym typowaniem  z szybkością C++. Oczekiwałem Pythona kompilowanego do kodu maszynowego. Po co? nie wiem. Kiedy okazało się, że jest to język projektowany do zupełnie innych celów, po jakimś czasie dałem sobie spokój.

Minęło lat parę i przez przypadek trafiłem znów na stronę języka i stwierdziłem, że zobaczę co tam nowego. Starszy i bogatszy o parę lat doświadczenia, epizody z różnymi językami min erlangiem, ujrzałem:


  • język statycznie typowany
  • mimo to o dużej ekspresji
  • z wbudowanymi w język elementami do programowania współbieżnego, rodem z erlang
  • o bardzo szybkim kompilatorze
  • w którym mogę kompilować pod Linux i Windows
  • bez hierarchii dziedziczenia, ale z osadzaniem typów
  • o bogatej bibliotece standardowej
  • w którym można pisać i obiekty i funkcje
  • funkcje są obiektami 
Czyli podsumowując, język wręcz idealny do pisania backendu. 

Kilka słów

Organizacja kodu:

W go kod zorganizowany jest w pakiety. Każdy plik powinien zaczynać się deklaracją nazwy pakietu. Pakiet może się składać w wielu plików, które nie koniecznie muszą się znajdować w tym samym katalogu. Daje to pewną dowolność w organizowaniu źródeł. Ograniczeniem jest to, że w jednym pliku może znajdować się tylko jedna definicja pakietu.
Jeśli chcemy "wyeksportować" czy to zmienną, stałą czy też funkcje poza pakiet. Czyli aby była widoczna poza pakietem należy jej nazwę rozpocząć z wielkiej litery. Ot co.

Przykład:

---------------
package pumba

import (
    "fmt"
)

func Hello() {
    fmt.Println("Hakuna matata")
}
---------------

W ten sposób zdeklarowaliśmy pakiet pumba do którego zaimportowaliśmy pakiet fmt i wyeksportowaliśmy funkcję Hello, która nie przyjmuje parametrów i nic nie zwraca.


Aby zapoznać się z podstawami polecam tutoriale www.golang.org.