Ten artykuł to tłumaczenie, oryginał znajduje się pod adresem http://golang.org/doc/go_tutorial.htm i jest udostępniony na licencji: Creative Commons Attribution 3.0.

Tutorial

Wprowadzenie

To wstęp do języka Go, napisany jest z myślą o programistach znających języki C lub C++. Daleko mu do wyczerpującego podręcznika; na tą chwilę bardziej “podręcznikowa” jest specyfikacja języka. Po przyswojeniu wiedzy z tego tutoriala zachęcamy Cię do przeczytania Effective Go, który zagłębia w praktyczne aspekty używania języka. Polecamy również slajdy z trzydniowego szkolenia na temat Go: Day 1, Day 2, Day 3.

Poniżej prezentujemy kilka małych programów ukazujących kluczowe właściwości języka. Wszystkie programy działają i są umieszczone w repozytorium w katalogu /docs/progs

Wycinki programów posiadają numery linii w oryginalnym pliku; dla jasności: puste linie pozostawiliśmy puste.

Hello, World

Zacznijmy od standardowego przykładu:

05    package main

07    import fmt "fmt"  // Pakiet implementujący formatowanie we-wy

09    func main() {
10        fmt.Printf("Hello, world; or Καλημέρα κόσμε; or こんにちは 世界\n")
11    }

Każdy plik źródłowy Go zaczyna się od deklaracji do jakiego pakietu należy, za pomocą wyrażenia package. Może także importować inne pakiety, których potrzebuje. Powyższy program importuje pakiet fmt, by mieć dostęp do starego dobrego, tu pisanego z wielkiej litery, fmt.Printf.

Definicję funkcji rozpoczynamy od słowa kluczowego “func”. Funkcja main z pakietu main jest miejscem w którym program rozpoczyna wykonywanie (oczywiście po wykonaniu wszelkich instrukcji inicjujących)

Łańcuchy znaków mogą zawierać znaki Unicode zakodowane w UTF-8 (pliki źródłowe Go są domyślnie w takim kodowaniu). Komentarze umieszczamy tak jak w C/C++

/* ... */
// ...

Później napiszemy znacznie więcej na temat wypisywania znaków.

Średniki

Zapewne zauważyłeś, że nasz program nie miał średników. Jedynym miejscem w kodzie Go gdzie można spotkać średniki to rozdzielenie klauzuli w deklaracji pętli for, i to też nie zawsze.

Tak naprawdę to formalnie język używa średników tak jak C czy Java, ale są wstawiane automatycznie na końcu każdej linii, która kończy wyrażenie. Po nudne szczegóły możesz sięgnąć do specyfikacji języka, ale zapamiętaj że możesz ale nie musisz używać średników.

Takie podejście daje czysto wyglądający, wolny od średników kod. Kolejną niespodzianką jest obowiązek umieszczania klamry otwierającej przy konstrukcjach takich jak if (for, select) w tej samej linii. Jeśli tego nie zrobisz możliwe, że kod Ci się nie skompiluje, lub da zaskakująco dziwny wynik. Język do pewnego stopnia wymusza styl używania klamer.

Kompilacja

Go jest językiem kompilowanym. W tej chwili są dwa kompilatory. Gccgo jest kompilatorem Go, który używa wsparcia Gcc. Jest też cała gama kompilatorów z rożnymi nazwami dla każdej architektury z osobna: 6g dla 64-bit x86, 8g dla 32 bit x86 i inne.

Te ostatnie kompilatory działają znacząco szybciej niż gccgo, ale dają mniej efektywny kod. W czasie pisania tego tutoriala (schyłek 2009), mają lepszy system bibliotek wykonywalnych, ale gccgo powoli je dogania.

Oto jak skompilować i uruchomić program. Za pomocą 6g:

$ 6g helloworld.go  # kompilacja; obiekty wędrują do helloworld.6
$ 6l helloworld.6   # łączenie; kod wynikowy ląduje w 6.out
$ 6.out
Hello, world; or Καλημέρα κόσμε; or こんにちは 世界
$

Kompilacja z użyciem gccgo wygląda trochę staroświecko:

$ gccgo helloworld.go
$ a.out
Hello, world; or Καλημέρα κόσμε; or こんにちは 世界
$

Echo

Teraz przyjrzymy się implementacji odpowiednika unixowej komendy echo(1):

05    package main

07    import (
08        "os"
09        "flag"  // parser opcji z linii poleceń
10    )

12    var omitNewline = flag.Bool("n", false, "nie wypisuj ostatniej nowej linii")

14    const (
15        Space = " "
16        Newline = "\n"
17    )

19    func main() {
20        flag.Parse()   // Skanuje listę argumentów i ustawia flagi
21        var s string = ""
22        for i := 0; i < flag.NArg(); i++ {
23            if i > 0 {
24                s += Space
25            }
26            s += flag.Arg(i)
27        }
28        if !*omitNewline {
29            s += Newline
30        }
31        os.Stdout.WriteString(s)
32    }

To krótki program, ale pojawiło się w nim kilka nowych rzeczy. W poprzednim przykładzie dowiedzieliśmy się, że func poprzedzało deklarację funkcji. Słowa kluczowe var, const i type (jeszcze nie używane) także są deklaracjami, tak jak import. Zauważ że możemy pogrupować deklaracje tego samego typu w listę otoczoną nawiasami, tak jak w liniach 7-10 i 14-17. Chociaż nie musimy tak robić, równie dobrze mogliśmy napisać

const Space = " "
const Newline = "\n"

Powyższy program importuje pakiet os aby mieć dostęp do zmiennej Stdout typu *os.File. Wyrażenie import jest w deklaracją: jak w przykładzie “hello world”, wskazuje identyfikator (fmt), który będzie udostępniał składowe pakietu zaimportowanego z pliku (“fmt”), który znajduje się w bieżącym katalogu lub w innej domyślnej lokalizacji

Nie mniej w tym programie, pominęliśmy w instrukcjach importu formalną nazwę pakietu. Domyślnie pakiety są identyfikowane pod nazwami zdefiniowanymi przez importowany pakiet, które zgodnie z konwencją są nazwami plików definiujących pakiet. Nasz program “hello world” mógł zawierać tylko wyrażenie import “fmt”.

Oczywiście możesz sam nadawać identyfikatory importowanych pakietów, ale to jest konieczne tylko wtedy, gdy pojawia się konflikt nazw.

By wydrukować łańcuch znaków możemy wykorzystać os.Stdout wywołując metodę WriteString.

Po zaimportowaniu pakietu “flag”, w linii 12 tworzymy zmienną globalną do przechowywania wartości parametru -n komendy echo. Zmienna omitNewline ma typ *bool - wskaźnik na wartość logiczną.

W main.main parsujemy argumenty wywołania (linia 20) po czym tworzymy lokalną zmienną, której użyjemy do przechowania danych wynikowych programu. Deklaracja tej zmiennej wyglądała tak:

var s string = ""

Zaczynamy słowem kluczowym var, po nim następuje nazwa zmiennej po czym następuje typ, po którym jest znak równości i wartość początkowa.

Go stara się być zwięzły i ta deklaracja może zostać skrócona. Dopóki literał w postaci łańcucha znaków jest typu “string” nie musimy o tym informować kompilatora. Możemy napisać

var s = ""

Albo jeszcze krócej:

s := ""

operator := jest używany bardzo często w Go, reprezentuje deklarację z jednoczesną inicjalizacją. Tak jak napisaliśmy w klauzuli for w następnej linii:

22        for i := 0; i < flag.NArg(); i++ {

Pakiet “flag” przechowuje sparsowane argumenty w liście którą można najzwyczajniej w świecie iterować.

Wyrażenie “for” różni się od tego znanego z języka C na kilka sposobów. Po pierwsze jest jedyną możliwą konstrukcją pętli; nie ma ani while ani do. Po drugie nie ma nawiasów otaczających klauzule, ale klamry otaczające ciało pętli są obowiązkowe. To samo tyczy się wyrażeń if i switch. Późniejsze przykłady pokażą inne sposoby deklarowania pętli for.

Ciało pętli konstruuje łańcuch znaków s przez dodawanie (używając operatora +=) argumentów wywołania i oddzielające spacje. Po zakończeniu pętli, jeśli flaga -n nie jest ustawiona, program dopisuje znak nowej linii. Ostatecznie wypisujemy rezultat.

Zauważ, że main.main jest funkcją nie przyjmującą żadnych argumentów i nie zwracającą wartości. Tak ją definiujemy. Wyjście z funkcji main.main oznacza sukces, jeśli chcesz zasygnalizować błędne zakończenie wywołaj:

os.Exit(1)

Pakiet os udostępnia także inne podstawowe obiekty; na przykład os.Args, jest wycinkiem (“slice”) używanym przez pakiet “flag”, który udostępnia argumenty wywołania z linii poleceń.

Słów kilka o typach

Go ma kilka znajomo wyglądających typów takich jak int, które reprezentują wartości “odpowiedniej” wielkości dla każdej maszyny. Istnieją oczywiście typy o określonej wielkości takie jak int8, float64, i tym podobne, oraz pozbawione znaku typy całkowite uint, uint32 itd. Są to różne typy; nawet jeśli int i int32 mają reprezentację 32 bitową, to nie są tego samego typu. Jest także typ byte, będący synonimem uint8, który jest podstawowym typem dla łańcuchów znaków (stringów).

Skoro napisaliśmy o stringach, to także typ wbudowany. Stringi są nie zmienne co do wartości - to nie są tylko tablice bajtów. Nie można zmienić wartości stringa, ale oczywiście możesz zmienić wartość zmiennej stringowej przez powtórne przypisanie jej. Wycinki z strings.go, które są prawidłowe:

11        s := "hello"
12        if s[1] != 'e' { os.Exit(1) }
13        s = "good bye"
14        var p *string = &s
15        *p = "ciao"

Jednak poniższe wyrażenia nie są już dozwolone, bo próbują zmieniać wartość stringa:

s[0] = 'x'
(*p)[1] = 'y'

Stringi w go są trochę jak const string w C++, o ile można powiedzieć że wskaźniki do stringów odpowiadają referencjom const string Tak, mamy także wskaźniki. Jednakże Go nieco uprasza ich użytkowanie; ale o tym później. Tablice deklarujemy tak:

var arrayOfInt [10]int

Tablice są wartościami, tak jak stringi, ale są zmienne. To różnica w stosunku do C, gdzie arrayOfInt było by używane jako wskaźnik do intów. Dopóki tablice są wartościami w Go, ma sens (i jest użyteczne) mówienie o wskaźnikach do tablic.

Rozmiar tablicy to część typu; można też zadeklarować wycinek (slice) który posiada referencję do dowolnej tablicy o dowolnym rozmiarze o tym samym typie elementów. Wyrażenie tworzące wycinek ma formę a[low:high], oznacza część tablicy od elementu o indeksie low aż do high-1; wynikowy wycinek jest indeksowany od 0 do high-low-1. W skrócie wycinki wyglądają podobnie do tablic, ale nie mają wskazanej wielkości ([] vs. [10]) i odnoszą się do fragmentu bazowej, przeważnie anonimowej, zwyczajnej tablicy. Wiele wycinków może współdzielić dane jeśli pochodzą z tej samej tablicy; wiele tablic nie może współdzielić danych.

Wycinki są bardziej popularne w Go niż zwykłe tablice; są bardziej elastyczne, mają postać referencyjną i są wydajne. To czego im brakuje to kontroli nad strukturą składowania danych (w pamięci - przyp tłum); jeśli chcesz mieć setki elementów tablicy wewnątrz swojej struktury to powinieneś użyć zwykłej tablicy. Aby taką stworzyć użyj konstruktora składającego wartości - wyrażenia złożonego z typu i otoczonego klamrami wyrażenia jak to:

[3]int{1,2,3}

Właśnie skonstruowaliśmy tablicę 3 liczb całkowitych.

W większości przypadków zamiast przekazywać tablicę do funkcji, lepiej zadeklarować parametr jako wycinek. Podczas wywołania funkcji przekaż wycinek z tej tablicy. Domyślnie dolne i górne ograniczenie wycinka przyjmuje wartości początku i końca istniejącego obiektu, więc ten [:] zwięzły zapis zwróci wycinek z całej tablicy.

Używając wycinków można napisać taką funkcję (z sum.go):

09    func sum(a []int) int { // returns an int
10        s := 0
11        for i := 0; i < len(a); i++ {
12            s += a[i]
13        }
14        return s
15    }

Zauważ, że typ wyniku (int) definiujemy przez umieszczenie go po liście parametrów. W wywołaniu tej funkcji użyjemy wycinka tablicy. W tym dziwne wyglądającym wywołaniu (niżej pokażemy nieco prostszą metodę) tworzymy tablicę i generujemy z niej wycinek:

s := sum([3]int{1,2,3}[:])

Jeśli tworzysz zwykłą tablicę, ale chcesz by to kompilator policzył ilość jej elementów za ciebie, użyj ... jako rozmiar tablicy

s := sum([...]int{1,2,3}[:])

To jest zamieszane bardziej niż potrzeba. W praktyce, chyba że bardzo zwracasz uwagę na ułożenie danych w strukturach, wszystko czego potrzebujesz to prosta deklaracja wycinka - za pomocą pustych nawiasów bez podania rozmiaru:

s := sum([]int{1,2,3})

Mamy także mapy, które możesz zainicjować w ten sposób:

m := map[string]int{"one":1 , "two":2}

Wbudowana funkcja len(), która pojawia się po raz pierwszy w funkcji sum, zwraca ilość elementów. Działa zarówno na tablicach, wycinkach, stringach, mapach i kanałach.

Przy okazji: inną rzeczą która działa dla tablic, wycinków, stringów, map i kanałów jest klauzula zasięgu (range) dla pętli for. Zamiast pisać:

for i := 0; i < len(a); i++ { ... }

aby przeiterować po elementach tablic (wycinków, stringów...), możemy napisać

for i, v := range a { ... }

To przypisuje zmiennej i wartość indeksu a zmiennej v wartość kolejnych elementów iterowanego obiektu

Po więcej przykładów sięgnij do Effective Go

Kilka słów o alokacji

Większość zmiennych w Go to wartości. Przypisanie inta, structa lub tablicy, do zmiennej kopiuje zawartość obiektu. Aby zaalokować nową zmienną, użyj new(), które zwróci wskaźnik do zaalokowanej pamięci.

type T struct { a, b int }
var t *T = new(T)

lub bardziej idiomatycznie:

t := new(T)

Niektóre typy - mapy, wycinki, kanały - mają referencyjną naturę. Jeśli modyfikujesz zawartość tego typu zmiennej inne zmienne posiadające referencję do tych danych zauważą tą zmianę. Do inicjowania zmiennych tych typów użyj funkcji wbudowanej make():

m := make(map[string]int)

To wyrażenie inicjuje nową mapę gotową do przechowywania wpisów. Jeśli tylko zadeklarujesz mapę tak:

var m map[string]int

to utworzy zerową referencję, która nie może przechowywać niczego. Aby używać map, musisz najpierw zainicjalizować referencję używając make() albo przypisać już istniejącą instancję map.

Zauważ, że new(T) zwraca typ *T, podczas gdy make zwraca typ T. Jeśli przez przypadek zaalokujesz referencję obiektu za pomocą new(), otrzymasz zerową referencję co jest równoznaczne z deklaracją niezainicjowanej zmiennej i pobraniem jej adresu.

Słów kilka o stałych.

O ile liczby całkowite mogą być wyrażone w różnych wielkościach o tyle stałe liczby całkowite nie. Nie ma takich stałych jak 0LL czy 0x0UI. Za to stałe całkowite są ewaluowane jako wartości wielkiej precyzji, które mogą przekroczyć zakres tylko wtedy gdy przypiszemy je do zmiennej całkowitej ze zbyt małą precyzją.

const hardEight = (1 << 100) >> 97  // legal

Istnieją pewne niuanse, które zasługują na odesłanie do specyfikacji języka, ale tu pokażemy kilka przykładów.

var a uint64 = 0  // ma typ uint64, wartość 0
a := uint64(0)    // równoznaczne z powyższym; wykorzystuje "konwersję"
i := 0x1234       // i dostaje domyślnie typ int
var j int = 1e6   // dozwolone - 1000000 jest reprezentowalne jako int
x := 1.5          // float – liczba zmiennoprzecinkowa
i3div2 := 3/2     // wynik dzielenia liczb całkowitych – wynik 1
f3div2 := 3./2.   // wynik dzielenia liczb zmiennoprzecinkowych – wynik 1.5

Konwersje działają tylko w prostych przypadkach takich jak zmiana liczb całkowitych jednego znaku czy rozmiaru w drugi, między liczbami całkowitymi a zmiennoprzecinkowymi, oraz w kilku innych prostych przypadkach. Nie ma żadnych automatycznych konwersji numerycznych w Go poza przypadkiem przypisania stałej do zmiennej o zadeklarowanego typie.

Pakiet wejścia-wyjścia (I/O)

Teraz przyjrzymy się prostemu pakietowi wykonującego operacje we-wy (wejścia-wyjścia) za pomocą zwykłego interfejsu typu open/close/read/write. To pierwsze linijki file.go

05    package file

07    import (
08        "os"
09        "syscall"
10    )

12    type File struct {
13        fd   int    // numer deskryptora
14        name string // nazwa pliku do otwarcia
15    }

W pierwszych linijkach znajduje się deklaracja nazwy pakietu - file - oraz import dwóch innych (os, syscall). Pakiet os zaciera różnice między różnymi systemami operacyjnymi, aby dać ujednoliconą obsługę plików i innych typowo systemowych operacji; tu zaś będziemy używali os’owych mechanizmów obsługi błędów i reprodukowali zawiłości operacji wejścia-wyjścia.

Drugi import to nisko poziomowy pakiet syscall, który daje prosty interface do wywołań systemowych.

Poniżej znajduje się definicja typu: słówko type rozpoczyna deklarację typów tym przypadku strukturę nazwaną File. Aby było trochę ciekawiej, nasz File zawiera nazwę pliku do którego deskryptor się odwołuje

Ponieważ File rozpoczyna się od wielkiej liter, typ ten jest dostępny spoza pakietu, np. w pakiecie zdefiniowanym przez użytkownika. Zasada widzialności informacji w Go jest prosta: jeśli nazwa (typu, funkcji, metody, zmiennej, stałej czy pola w strukturze) zaczyna się wielką literą, użytkownicy pakietu mogą z niej korzystać. W przeciwnym przypadku nazwa, a zatem rzecz nazwana, będzie widoczna tylko w pakiecie w którym została zadeklarowana. To jest coś więcej niż konwencja; to zasada wymuszona przez kompilator. W Go zmienne widoczne publicznie nazywamy wyeksportowanymi.

W przypadku File, jego wszystkie pola są z małej litery, zatem są nie widoczne dla użytkowników, ale kilka linijek niżej dodamy wyeksportowane - pisane z wielkiej litery - metody.

Ale najpierw napiszmy funkcję fabrykującą File:

17    func newFile(fd int, name string) *File {
18        if fd < 0 {
19            return nil
20        }
21        return &File{fd, name}
22    }

Powyższa funkcja zwróci wskaźnik do struktury nowego pliku z wypełnionymi polami: deskryptora i nazwą. Powyższy kod wykorzystuje pojęcie literałów złożonych, takich jakich używaliśmy do budowania map, tablic, czy też nowego obiektu zaalokowanego na stosie. Możemy napisać:

n := new(File)
n.fd = fd
n.name = name
return n

ale dla prostych struktur takich jak plik łatwiej jest zwrócić adres złożonego literału, tak jak to zrobiliśmy w linijce 21

Możemy użyć funkcji fabrykującej aby skonstruować kilka znanych, wyeksportowanych zmiennych typu *File:

24    var (
25        Stdin  = newFile(0, "/dev/stdin")
26        Stdout = newFile(1, "/dev/stdout")
27        Stderr = newFile(2, "/dev/stderr")
28    )

Funkcja newFile nie została wyeksportowana, ponieważ jest do użytku wewnętrznego. Właściwą, wyeksportowaną, metodą fabrykującą jest Open:

30    func Open(name string, mode int, perm uint32) (file *File, err os.Error) {
31        r, e := syscall.Open(name, mode, perm)
32        if e != 0 {
33            err = os.Errno(e)
34        }
35        return newFile(r, name), err
36    }

Jest tu trochę nowych zagadnień w tych kilku linijkach. Po pierwsze, Open zwraca wiele wartości, File i error (błąd, więcej o błędach za chwilę). Deklarujemy fakt zwracania wielu wartości przez deklarację listy zmiennych (z typami, lub samych typów - przyp. tłum.) otoczonych nawiasami; składniowo wgląda to jak druga lista parametrów wywołania. Funkcja syscall.Open także zwraca wiele wartości które możemy przechwycić za pomocą przypisania wielu zmiennych do jej wyniku - tak jak w linijce 31; deklarujemy r i e aby przechować te wartości, obie typu int (by się o tym przekonać musiałbyś spojrzeć do pakietu syscall). W końcu linia 35 zwraca dwie wartości: wskaźnik do nowego obiektu File i błędu. Jeśli syscall.Open się nie powiedzie, deskryptor r będzie ujemny i newFile zwróci nil

Odnośnie tych błędów. Biblioteka os przedstawia ogólny styl przekazywania błędów. Polecamy ci abyś przekazywał błędy w swoich interfejsach w ten właśnie sposób, dla zachowania spójności obsługi błędów w całym kodzie napisanym w Go. W funkcji Open zastosowaliśmy konwencję przedstawiania Uniksowego numeru błędu w postaci typu os.Errno, który implementuje (interface) os.Error.

Zatem możemy tworzyć zmienne typu File i możemy pisać dla nich metody. Aby zdefiniować metodę typu, tworzymy funkcję dla której określamy odbiorcę - przed nazwą funkcji umieszczamy w nawiasach nazwę zmiennej i typ odbiorcy. Poniżej kilka metod dla *File, każda z nich deklaruje zmienną odbiorcy:

38    func (file *File) Close() os.Error {
39        if file == nil {
40            return os.EINVAL
41        }
42        e := syscall.Close(file.fd)
43        file.fd = -1 // aby nie mógł zostać zamknięty ponownie
44        if e != 0 {
45            return os.Errno(e)
46        }
47        return nil
48    }

50    func (file *File) Read(b []byte) (ret int, err os.Error) {
51        if file == nil {
52            return -1, os.EINVAL
53        }
54        r, e := syscall.Read(file.fd, b)
55        if e != 0 {
56            err = os.Errno(e)
57        }
58        return int(r), err
59    }

61    func (file *File) Write(b []byte) (ret int, err os.Error) {
62        if file == nil {
63            return -1, os.EINVAL
64        }
65        r, e := syscall.Write(file.fd, b)
66        if e != 0 {
67            err = os.Errno(e)
68        }
69        return int(r), err
70    }

72    func (file *File) String() string {
73        return file.name
74    }

Nie ma czegoś takiego jak niejawne “this”, aby dostać się do składowych danej struktury musimy użyć zmiennej odbiorcy.

Metody nie są deklarowane wewnątrz deklaracji struct. Deklaracja struct określa tylko dane składowe. W zasadzie metody mogą być tworzone dla każdego typu, który przekażesz w deklaracji odbiorcy, nawet dla int czy Arrauy, nie tylko dla struktur. Później pokażemy jak to zrobić na przykładzie tablic.

Nadaliśmy metodzie String taką a nie inną nazwę ze względu na konwencje związane z wypisywaniem (printowaniem) o których napiszemy później. Te metody używają publicznej zmiennej os.EINVAL aby zwrócić uniksowy kod błędu EINVAL (który jest instancją os.Error). Standardowy zestaw takich wartości definiuje pakiet os.

Zobaczmy ten nowy pakiet w użyciu.

05    package main

07    import (
08        "./file"
09        "fmt"
10        "os"
11    )

13    func main() {
14        hello := []byte("hello, world\n")
15        file.Stdout.Write(hello)
16        file, err := file.Open("/does/not/exist",  0,  0)
17        if file == nil {
18            fmt.Printf("can't open file; err=%s\n",  err.String())
19            os.Exit(1)
20        }
21    }

Znaki “./” w wyrażeniu importu “./file” informuje kompilator by używał naszej (lokalnej) wersji pakietu a nie tej zainstalowanej w katalogu go. (“file.go”, musi zostać skompilowane przed importem)

Skompilujemy i uruchomimy nasz program:

$ 6g file.go                       # kompilacja pakietu file
$ 6g helloworld3.go         # kompilacja pakietu main
$ 6l -o helloworld3 helloworld3.6  # łączenie - nie ma potrzeby podawania “file.6”
$ helloworld3
hello, world
can't open file; err=No such file or directory
$

Rotting cats

Wykorzystując pakiet file, napiszemy prościutką wersję programu uniksowego programu cat(1).

progs/cat.go:

05    package main

07    import (
08        "./file"
09        "flag"
10        "fmt"
11        "os"
12    )

14    func cat(f *file.File) {
15        const NBUF = 512
16        var buf [NBUF]byte
17        for {
18            switch nr, er := f.Read(buf[:]); true {
19            case nr < 0:
20                fmt.Fprintf(os.Stderr, "cat: error reading from %s: %s\n", f.String(), er.String())
21                os.Exit(1)
22            case nr == 0:  // EOF
23                return
24            case nr > 0:
25                if nw, ew := file.Stdout.Write(buf[0:nr]); nw != nr {
26                    fmt.Fprintf(os.Stderr, "cat: error writing from %s: %s\n", f.String(), ew.String())
27                }
28            }
29        }
30    }

32    func main() {
33        flag.Parse()   // Przegląda listę argumentów wywołania i ustawia flagi
34        if flag.NArg() == 0 {
35            cat(file.Stdin)
36        }
37       for i := 0; i < flag.NArg(); i++ {
38            f, err := file.Open(flag.Arg(i), 0, 0)
39            if f == nil {
40                fmt.Fprintf(os.Stderr, "cat: can't open %s: error %s\n", flag.Arg(i), err)
41                os.Exit(1)
42            }
43            cat(f)
44            f.Close()
45        }
46    }

Jak na razie wszystko powinno być jasne, chociaż switch wprowadza kilka nowości. Tak jak for-y i if-y switch-e mogą zawierać wyrażenie inicjujące. Switch w linijce 18 wykorzystuje wyrażenie inicjujące by przypisać wartości zwracane z f.Read() do zmiennych nr i er . (podobny zabieg jest przeprowadzony w linijce 25). Switch zachowuje się następująco: przetwarza przypadki (case'y) zaczynając od góry w poszukiwaniu pierwszego który odpowiada wartości. Wyrażenia w case nie muszą być stałymi ani nawet liczbami całkowitymi, najważniejsze by zwracały dane tego samego typu.

Jeśli wartość w switch jest true, możemy ją pominąć – tak jak to jest w wyrażeniu for, brakująca wartość oznacza true. W zasadzie taki switch jest rodzajem łańcuchów if-else. A skoro już o tym piszemy, to nadmienimy że w switch'u każdy case kończy się niejawnym przerwaniem (break).

W linii 25 w wywołaniu Write() przekazujemy wycinek bufora wejściowego, który sam z siebie jest wycinkiem. Wycinki wprowadzają typowy dla Go sposób obsługiwania buforów We-Wy.

Zróbmy teraz inną wersję programu cat, która opcjonalnie wykonuje rot13 na danych wejściowych. Można to zrobić bardzo łatwo i szybko przesuwając bajty, ale zamiast tego zgłębimy interfejsy Go.

Metoda cat używa dwóch metod obiektu f: Read() i String(), zatem zdefiniujemy interface, który ma tylko te dwie metody. Poniżej kod z progs/cat_rot13.go:

26    type reader interface {
27        Read(b []byte) (ret int, err os.Error)
28        String() string
29    }

Każdy typ, który ma te dwie metody mówimy, że implementuje interface readera (niezależnie od innych metod, które ten typ może udostępniać). A skoro file.File zawiera te metody, to oznacza że implementuje interface readera. Możemy zmienić metodę cat tak aby przyjmowała readery zamiast *file.File i wszystko ciągle będzie grało, ale najpierw stwórzmy drugi typ implementujący interface readera, który opakowuje istniejącego readera i wykonuje rot13 na danych. Zdefiniujmy typ i zaimplementujmy metody to otrzymamy kolejną implementację interfejsu reader.

31    type rotate13 struct {
32        source    reader
33    }

35    func newRotate13(source reader) *rotate13 {
36        return &rotate13{source}
37    }

39    func (r13 *rotate13) Read(b []byte) (ret int, err os.Error) {
40        r, e := r13.source.Read(b)
41        for i := 0; i < r; i++ {
42            b[i] = rot13(b[i])
43        }
44        return r, e
45    }

47    func (r13 *rotate13) String() string {
48        return r13.source.String()
49    }
50    // end of rotate13 implementation

(Funkcja rot13 użyta w linii 42 jest trywialna i nie ma sensu zamieszczać jej tutaj.) Aby wykorzystać nową funkcjonalność zdefiniujemy flagę rot13:

14    var rot13Flag = flag.Bool("rot13", false, "rot13 the input")

i posłużymy się nią w lekkiej modyfikacji funkcji cat:

52    func cat(r reader) {
53        const NBUF = 512
54        var buf [NBUF]byte

56        if *rot13Flag {
57            r = newRotate13(r)
58        }
59        for {
60            switch nr, er := r.Read(buf[:]); {
61            case nr < 0:
62                fmt.Fprintf(os.Stderr, "cat: error reading from %s: %s\n", r.String(), er.String())
63                os.Exit(1)
64            case nr == 0:  // EOF
65                return
66            case nr > 0:
67                nw, ew := file.Stdout.Write(buf[0:nr])
68                if nw != nr {
69                    fmt.Fprintf(os.Stderr, "cat: error writing from %s: %s\n", r.String(), ew.String())
70                }
71            }
72        }
73    }

(Moglibyśmy zrobić to opakowywanie w funkcji main i zostawić cat() w spokoju zmieniając tylko parametry wywołania; rozważ ten sposób w ramach ćwiczeń).

Cała myk jest wykonany w ifie na początku funkcji cat: jeśli flaga rot13 jest ustawiona na true, opakuj readera w zrotowanego readera i idź dalej. Zauważ, że zmienne zawierające typ interfejsu są wartościami a nie wskaźnikami: typ argumentu to reader a nie *reader, nawet jeśli skrycie zawiera wskaźnik do struktury.

Tak to wygląda w praniu:

$ echo abcdefghijklmnopqrstuvwxyz | ./cat
abcdefghijklmnopqrstuvwxyz
$ echo abcdefghijklmnopqrstuvwxyz | ./cat --rot13
nopqrstuvwxyzabcdefghijklm
$

Zwolennicy wstrzykiwania zależności powinni się cieszyć z tego jak łatwo interfejsy pozwalają nam podmienić implementację deskryptora plików.

Interfejsy to kluczowa konstrukcja w Go. Typ implementuje interface jeśli tylko zawiera wszystkie metody odpowiadające tym zadeklarowanym w interfejsie. To oznacza, że typ może implementować niezliczoną ilość różnych interfejsów. Nie ma hierarchii typów; obiekty mogą być zmieniane za zawołanie tak jak to zrobiliśmy z rot13. Typ file.File implementuje interface readera; może także implementować writera i dowolny inny interfejs składający się z jego metod pasujących do danej sytuacji. Spójrz teraz na pusty interface:

type Empty interface {}

Każdy typ spełnia wymagania pustego interfejsu, to przydatna rzecz do implementacji różnego typu kontenerów

Sortowanie

Interfejsy wprowadzają bardzo prostą formę polimorfizmu. Całkowicie separują definicję tego co obiekt robi od tego jak to robi, umożliwiając wartościom różnych typów reprezentację przez tą samą zmienną danego interfejsu.

Jako przykład, rozważ ten prosty algorytm sortowania wzięty z progs/sort.go:

13    func Sort(data Interface) {
14        for i := 1; i < data.Len(); i++ {
15            for j := i; j > 0 && data.Less(j, j-1); j-- {
16                data.Swap(j, j-1)
17            }
18        }
19    }

Ten kod wymaga tylko trzech metod, które opakujemy w interface

07    type Interface interface {
08        Len() int
09        Less(i, j int) bool
10        Swap(i, j int)
11    }

Teraz możemy zastosować Sort na każdym typie, który implementuje metody Len, Less i Swap. Pakiet sort zbiór funkcji umożliwiających sortowanie tablic intów, stringów itd; poniżej kod dla tablicy intów:

33    type IntArray []int

35    func (p IntArray) Len() int            { return len(p) }
36    func (p IntArray) Less(i, j int) bool  { return p[i] < p[j] }
37    func (p IntArray) Swap(i, j int)       { p[i], p[j] = p[j], p[i] }

To są metody zdefiniowane dla typów nie będących structami. Ty jednak możesz zdefiniować metody dla dowolnego typu, który zdefiniujesz i nazwiesz w swoim pakiecie.

Teraz funkcja testująca, z progs/sortmain.go. Używa funkcji z pakietu sort, aby sprawdzić czy wynik jest posortowany

12    func ints() {
13        data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}
14        a := sort.IntSlice(data)
15        sort.Sort(a)
16        if !sort.IsSorted(a) {
17            panic("fail")
18        }
19    }

Jeśli chcielibyśmy by kolejny typ był sortowalny, to wystarczy zaimplementować dla niego te trzy metody. Np. tak jak poniżej:

30    type day struct {
31        num        int
32        shortName  string
33        longName   string
34    }

36    type dayArray struct {
37        data []*day
38    }

40    func (p *dayArray) Len() int            { return len(p.data) }
41    func (p *dayArray) Less(i, j int) bool  { return p.data[i].num < p.data[j].num }
42    func (p *dayArray) Swap(i, j int)       { p.data[i], p.data[j] = p.data[j], p.data[i] }

Drukowanie

Dotychczasowe przykłady formatowania wydruku były powściągliwe. W tej sekcji opiszemy jak dobrze można formatować dane we-wy w Go.

Widzieliśmy proste zastosowania pakietu fmt, który implementuje Printf, Fprintf i tak dalej. W paczce fmt, Printf jest zadeklarowane z poniższą sygnaturą:

Printf(format string, v ...interface{}) (n int, errno os.Error)

Token ... oznacza zmienną ilość argumentów, co w C było by obsłużone dzięki makrom stdarg.h. W Go funkcjom o zmiennej ilości argumentów przekazujemy wycinek argumentów określonego typu. W przypadku Printf deklaracja stanowi ...interface{}, więc typ v jest wycinkiem pustych interfejsów ([]interface{}). Printf może zbadać typ każdego z tych argumentów za pomocą tzw. “type switcha” albo dzięki bibliotece reflection, iterując po elementach wycinka. Na marginesie: taka analiza typu w czasie wykonywania pomaga wyjaśnić kilka ciekawych właściwości Printf, takich jak wykrywanie dynamicznych typów swoich argumentów.

Na przykład, w C formatowanie musi odpowiadać typom argumentów. W wielu przypadkach w Go jest to prostsze. Zamiast %llud możesz napisać %d; Printf zna rozmiar i znak inta i może “zrobić dobrze” za ciebie. Ten snippet

10        var u64 uint64 = 1<<64-1
11        fmt.Printf("%d %d\n", u64, int64(u64))

wydrukuje

18446744073709551615 -1

W zasadzie jeśli jesteś leniwy format %v wydrukuje, w prostym odpowiednim stylu, każdą wartość, także tablice i strukturę.

14        type T struct {
15            a int
16            b string
17        }
18        t := T{77, "Sunset Strip"}
19        a := []int{1, 2, 3, 4}
20        fmt.Printf("%v %v %v\n", u64, t, a)

da

18446744073709551615 {77 Sunset Strip} [1 2 3 4]

Możesz zaniechać formatowania jeśli używasz Print lub Prinln zamiast Printf. Te funkcje w pełni zautomatyzują formatowanie. Print wypisuje elementy używając formatowania równoznacznego z %v, zaś Println wstawia spacje pomiędzy argumentami i dodaje znak nowej linii. Na wyjściu obie linie dadzą taki sam wynik jak Printf powyżej:

21        fmt.Print(u64, " ", t, " ", a, "\n")
22        fmt.Println(u64, t, a)

Jeśli chciałbyś panować nad formatowaniem własnego typu dodaj mu metodę String(), która zwraca odpowiednio sformatowanego stringa. Funkcje wypisujące sprawdzą czy wartości do nich przekazane implementują tą metodę i jeśli tak użyją jej a nie własnego formatowania. Poniżej prosty przykład:

09    type testType struct {
10        a int
11        b string
12    }

14    func (t *testType) String() string {
15        return fmt.Sprint(t.a) + " " + t.b
16    }

18    func main() {
19        t := &testType{77, "Sunset Strip"}
20        fmt.Println(t)
21    }

Jeśli testType implementuje metodę String(), to domyślny formater wywoła ją i zwróci takie oto wyjście:

77 Sunset Strip

Zauważ, że String() wywołuje Sprint aby wykonać formatowanie; specjalne formatery mogą używać biblioteki fmt rekursywnie. Kolejną cechą Printf jest to, że %T wydrukuje reprezentację znakową typu wartości zmiennej, co może być przydatne przy debugowaniu polimorficznego kodu. Jest możliwość zbudowania pełnych formatowań z flagami, precyzjami i takimi tam, ale to wykracza poza główny wątek, więc zostawiam ten temat do samodzielnej eksploracji. Możesz się zastanawiać skąd Printf wie czy typ implementuje metodę String(). Tak naprawdę to on sprawdza czy wartość może być rzutowana na zmienną o odpowiednim interface. Schematycznie dla wartości v robi to:

type Stringer interface {
    String() string
}

s, ok := v.(Stringer)  // Test whether v implements "String()"
if ok {
    result = s.String()
} else {
    result = defaultOutput(v)
}

Aby sprawdzić czy wartość w v spełnia wymagania interfejsu Stringer ten kod wykorzystuje asercję typów (v.(Stringer)); jeśli spełnia to s staje się zmienną typu “interfejsowego” czyli implementującą interface Stringer a ok będzie miało wartość true. Po czym możemy użyć s by wywołać metodę String(). (Wzorzec “comma, ok” jest idiomatyczny dla Go używany do sprawdzenia powodzenia takich operacji jak konwersja typów, mapowanie, komunikacja i tak dalej, nie mniej jest to jedyne miejsce w tym tutorialu, które o tym wspomina.) Jeśli wartość nie spełnia interfejsu to ok będzie miało wartość false.

W powyższym przykładzie nazwa Stringer to kolejna konwencja, w której dodajemy [e]r do nazwy interfejsu opisującego tak proste zestawy funkcji jak te.

Ostatnia sprawa. Dla pełnego obrazu, poza Printf i Sprintf itd., są jeszcze Fprintf i tego typu funkcje. Inaczej niż w C, pierwszym argumentem Fprintf nie jest plik. Zamiast tego jest zmienna typu io.Writer, który jest typem interfejsu zdefiniowanym w pakiecie io:

type Writer interface {
    Write(p []byte) (n int, err os.Error)
}

(Ten interface to kolejny przykład nazwy zgodnej z powyższą konwencją, tym razem dla write (zapisz); są także io.Reader, io.ReadWriter i tak dalej). Zatem możesz wywołać Fprintf na każdym typie który implementuje standardową metodę Write(), nie tylko na plikach ale także na kanałach sieciowe, buforach, czymkolwiek.

Liczby pierwsze

Przejdziemy teraz do procesów i komunikacji - programowania współbieżnego. To trudny i długi temat, więc by nieco skrócić zakładamy ogólnikową znajomość tych zagadnień.

Klasycznym programem tego typu jest wyliczanie liczb pierwszych. (Sito Eratostenesa jest obliczeniowo bardziej wydajne niż poniższy algorytm, ale tym razem chodzi nam o przedstawienie programowania współbieżnego a nie o algorytmikę.) Zasadą działania tego programu jest przepuszczenie strumienia liczb naturalnych przez sekwencję filtrów, po jednym dla każdej znalezionej liczby pierwszej, by wykluczyć jej wielokrotności. W każdym kroku dostajemy sekwencje filtrów już znalezionych liczb pierwszych, a gdy znajdziemy kolejną dodamy kolejny filtr do łańcucha. Poniżej diagram przepływu, każdy prostokąt reprezentuje filtr, którego utworzenie zostało zainicjowane przez pierwszą liczbę która przedarła się przez wszystkie poprzednie filtry.

///filtersimg

Do stworzenia strumienia intów, użyjemy typu channel (kanał), który, zapożyczony z potomków CSP, reprezentuje kanał komunikacyjny mogący połączyć równoległe obliczenia. W Go zmienne typu channel są referencjami do obiektu koordynującego komunikacje; podobnie jak map i wycinków, tworzymy instancje kanałów przez funkcję make.

Poniżej pierwsza funkcja z progs/sieve.go:

09    // Send the sequence 2, 3, 4, ... to channel 'ch'.
10    func generate(ch chan int) {
11        for i := 2; ; i++ {
12            ch <- i  // Send 'i' to channel 'ch'.
13        }
14    }

Funkcja generate wysyła sekwencje 2,3,4,5,.. do kanału, który przyjęła w argumencie wywołania i używa binarnego operatora <- do komunikacji. Operacje na kanałach są blokujące więc jeśli nie ma odbiorcy wartości, to sama operacja wysyłania poczeka dopóki ktoś tej wartości nie odbierze.

Funkcja filter (filtrująca) przyjmuje trzy argumenty: kanał wejściowy, kanał wyjściowy i liczbę pierwszą. Kopiuje wartości z wejścia na wyjście pomijając wszystko co jest podzielne przez tą liczbę pierwszą. Operator <-(odbierz), pobiera następną wartość z kanału.

16    // Copy the values from channel 'in' to channel 'out',
17    // removing those divisible by 'prime'.
18    func filter(in, out chan int, prime int) {
19        for {
20            i := <-in  // Receive value of new variable 'i' from 'in'.
21            if i % prime != 0 {
22                out <- i  // Send 'i' to channel 'out'.
23            }
24        }
25    }

Generator i filtry wykonują się równocześnie. Go ma własny model procesów/wątków/mikro procesów, więc by uniknąć zamieszania nazewniczego równoległe obliczenia nazywamy w Go gorutynami. Aby rozpocząć wykonywanie gorutyny wywołujemy funkcję poprzedzając ten fakt słówkiem go; to uruchamia funkcję równolegle do bieżących obliczeń ale w tej samej przestrzeni adresowej.

go sum(hugeArray) // calculate sum in the background

Jeśli chcesz wiedzieć kiedy obliczenia się skończą, przekaż kanał do niej, on ci da znać :-)

ch := make(chan int)
go sum(hugeArray, ch)
// ... do something else for a while
result := <-ch  // wait for, and retrieve, result

Wracając do generatora liczb pierwszych. Poniżej pokazujemy jak posklejaliśmy powyższe klocki:

28    func main() {
29        ch := make(chan int)  // Create a new channel.
30        go generate(ch)  // Start generate() as a goroutine.
31        for {
32            prime := <-ch
33            fmt.Println(prime)
34            ch1 := make(chan int)
35            go filter(ch, ch1, prime)
36            ch = ch1
37        }
38    }

W linii 29 tworzymy początkowy kanał który przekazujemy do funkcji generate, która startuje równolegle jako gorutyna. Za każdym razem gdy z kanału wyskakuje jakaś liczba nowy filtr jest dodawany do przebiegu i jego wyjście staje się nową wartością ch.

To sito może zostać dostosowane tak by używało znanych wzorców w tym stylu programowania. Poniżej jest inna wersja generate, z progs/sieve1.go:

10    func generate() chan int {
11        ch := make(chan int)
12        go func(){
13            for i := 2; ; i++ {
14                ch <- i
15            }
16        }()
17        return ch
18    }

Ta wersja robi całą inicjację wewnętrznie. Tworzy kanał wynikowy, odpala gorutunę będącą literałem funkcyjnym i zwraca kanał do wywołującego tą funkcję. To jest fabryka dla równoległych wykonań, wystartować gorutynę i zwrócić połączenie do niej.

Notacja literału funkcyjnego (linie 12-16) pozwala nam na konstruowanie funkcji anonimowych i wykonywania ich w miejscu. Zauważ, że lokalna zmienna ch jest dostępna dla tej funkcji i istnieje w niej nawet po zwrocie wyników z funkcji generate.

Taka sama zmiana może być uczyniona filtrom:

21    func filter(in chan int, prime int) chan int {
22        out := make(chan int)
23        go func() {
24            for {
25                if i := <-in; i % prime != 0 {
26                    out <- i
27                }
28            }
29        }()
30        return out
31    }

Pętla główna funkcji sieve staje się prostsza i “czystsza”, zmieńmy ją w fabrykę także!

33    func sieve() chan int {
34        out := make(chan int)
35        go func() {
36            ch := generate()
37            for {
38                prime := <-ch
39                out <- prime
40                ch = filter(ch, prime)
41            }
42        }()
43        return out
44    }

Teraz main dostaje wyniki sita w postaci kanału liczb pierwszych:

46    func main() {
47        primes := sieve()
48        for {
49            fmt.Println(<-primes)
50        }
51    }

Multiplexing

Za pomocą kanałów można obsłużyć wiele niezależnych gorutyn klienckich bez pisania multipleksera. Sztuczka polega na tym by do serwera przesłać kanał w wiadomości, który zostanie wykorzystany do wysłania odpowiedzi do nadawcy. Życiowy program typu klient-serwer to sporo kodu, zatem przedstawimy bardzo prosty przykład, by tylko zilustrować ideę. Zacznijmy od zdefiniowania typu request, który zawiera kanał, który będzie użyty do odpowiedzi:

09    type request struct {
10        a, b    int
11        replyc  chan int
12    }

Serwer będzie prościutki: będzie robił prostą binarną operację na liczbach całkowitych. Poniżej kod wywołujący operację i odpowiadający na zapytanie:

14    type binOp func(a, b int) int

16    func run(op binOp, req *request) {
17        reply := op(req.a, req.b)
18        req.replyc <- reply
19    }

Linia 14 definiuje binOp jako funkcję przyjmującą dwa integery i zwracającą trzeci. Funkcja serwera ma nieskończoną pętle, która otrzymuje żądania i, by zaradzić blokowaniu w trakcie długo trwających operacji, startuje gorutynę, która wykona to co należy.

21    func server(op binOp, service chan *request) {
22        for {
23            req := <-service
24            go run(op, req)  // don't wait for it
25        }
26    }

Tworzymy serwer w znajomy już sposób tworząc i zwracając kanał który jest z nim powiązany.

28    func startServer(op binOp) chan *request {
29        req := make(chan *request)
30        go server(op, req)
31        return req
32    }

Tutaj jest prosty test. Odpala serwer z operatorem dodawania i wysyła N żądań nie czekając na odpowiedzi. Dopiero gdy wszystkie żądania są wysłane, sprawdzamy wyniki.

34    func main() {
35        adder := startServer(func(a, b int) int { return a + b })
36        const N = 100
37        var reqs [N]request
38        for i := 0; i < N; i++ {
39            req := &reqs[i]
40            req.a = i
41            req.b = i + N
42            req.replyc = make(chan int)
43            adder <- req
44        }
45        for i := N-1; i >= 0; i-- {   // doesn't matter what order
46            if <-reqs[i].replyc != N + 2*i {
47                fmt.Println("fail at", i)
48            }
49        }
50        fmt.Println("done")
51    }

Denerwujące w tym programie jest to, że nie kończy działania serwera w czysty sposób; gdy funkcja main zwraca wartość żyją jeszcze te wolniejsze gorutyny, które zblokowały się na komunikacji. Aby to naprawić możemy dołączyć drugi kanał wyłączający serwer:

32    func startServer(op binOp) (service chan *request, quit chan bool) {
33        service = make(chan *request)
34        quit = make(chan bool)
35        go server(op, service, quit)
36        return service, quit
37    }

Przekazujemy kanał wyłączający do funkcji serwującej, która wykorzystuje go tak:

21    func server(op binOp, service chan *request, quit chan bool) {
22        for {
23            select {
24            case req := <-service:
25                go run(op, req)  // don't wait for it
26            case <-quit:
27                return
28            }
29        }
30    }

W funkcji server, wyrażenie select wybiera który z wielu kanałów komunikacji w wylistowanych case’ach może być wykorzystany, jeśli oba są zablokowane czeka aż któryś się odblokuje; jeśli kilka na raz jest gotowych to wybiera losowy. w tym przypadku server obsługuje zapytania póki nie dostanie żądania wyjścia, kiedy to po prostu zwraca wynik, kończąc wykonywanie.

To co zostało do dopisania to kanał zamykający pod koniec main:

40        adder, quit := startServer(func(a, b int) int { return a + b })

...

55        quit <- true

Należało by napisać dużo więcej o współbieżnym programowaniu w Go ale to krótkie wprowadzenie powinno dać garść podstaw.