Go - nový programovací jazyk od Google
 x   TIP: Přetáhni ikonu na hlavní panel pro připnutí webu

Go - nový programovací jazyk od GoogleGo - nový programovací jazyk od Google

 

Go - nový programovací jazyk od Google

Google       Google       30. 11. 2009       35 015×

Že jste ještě neslyšeli o tom „novém jazyku od Googlu“? V tomto článku se podíváme, jak Go získat, co nabízí, a nebudou samozřejmě chybět nějaké ukázkové kódy.

Go je nedávno uvolněný staticky typovaný kompilovaný jazyk, který vznikl v laboratořích Google. Na úvod trochu úsměvná věc; možná se nakonec ani nebude jmenovat Go kvůli issue číslo 9 – jeden jazyk s názvem Go totiž už existuje.

Od čtenáře se očekává, že se alespoň trochu orientuje v programování v imperativních jazycích. Účelem článku není naučit vás programovat, ale představit nějaké zajímavé věci, které Go nabízí, a v čem se liší od ostatních jazyků. Nic není bez chyb, a tak ani Go ne. Hezké vlastnosti budou prezentované ještě hezčím kódem s vysvětlením a většina těch zlých / považovaných za zlé bude zatracena pár slovy („It‘s not a bug, it‘s a feature!“).

Instalace

Go je kompilovaný jazyk, a tak proto, abychom si s ním mohli začít hrát, budeme právě potřebovat kompilátor.

Zprvu je nutné říci, že pokud používáte systém Microsoft Windows, máte smůlu – žádný kompilátor pro Windows ještě není. Ve FAQ se dočtete, že port kompilátoru pro Windows by byl skvělý, ale tým je malý a nemá dostatek zdrojů, aby toto uskutečnil. Ovšem kompilátor je uvolněn pod BSD licencí, a proto pokud byste chtěli, můžete s portem na Windows pomoci.

Ale jedním dechem je třeba dodat, že není vše ztraceno. Na gofmt.com totiž najdete okénko, do kterého když zkopírujete kód a dáte zkompilovat, můžete program spustit, a tak minimálně vyzkoušet, co to umí, můžete. Stejně tak toho mohou využít lidé, kteří jsou líní kompilátor instalovat.

Nyní již něco pro ten zbytek. Momentálně jsou dva kompilátory pro Go – jeden napsaný Kenem Thompsonem, který kompiluje rychleji a tolik kód neoptimalizuje; a druhý, kterýžto je front-endem pro GCC. My se budeme zabývat tím prvním.

Budete potřebovat Mercurial k získání zdrojových kódů a GCC, libc, Bison, make a ed ke zkompilování kompilátoru.

Linux

Jestliže používáte některou z Linuxových distribucí, měly byste tyto programy najít jako balíčky. Dejte pozor na to, že u libc (nejspíše se bude jednat o Glibc) je potřeba mít nainstalovaný i tzv. „dev“/„devel“ balíček, tj. balíček, který obsahuje všechny potřebné hlavičkové soubory ke kompilaci programů.

Pro Debian a distribuce založené na něm je to např.:

$ sudo apt-get install gcc libc6-dev bison make ed

V Arch Linuxu pomocí pacmana:

$ sudo pacman -S gcc glibc bison make ed

Podobně to jistě půjde i ve vaší distribuci.

Mac OS X

Mercurial můžete získat z odkazovaných stránek. Ostatní sestavovací závislosti se dají nainstalovat jako součást Xcode (je možné, že je tam již budete mít). Každopádně je potřeba Xcode.

Proměnné prostředí

Jak jsem zjistil po několika hodinách zkoušení, bez nastavení těchto proměnných prostředí to opravdu nefunguje:

$GOROOT
Kořenový adresář stromu se zdrojovými kódy Go.
$GOOS
Operační systém. Pro Linux nastavte na linux pro Mac OS X na darwin.
$GOARCH
Architektura procesoru. V době psaní článku Go podporuje x86 32-bit (nastavte $GOARCH na 386), 64-bit (amd64) a ARM (arm).

Např. u mě jsem si, jelikož běžím na Linuxu a stroj je 32-bitové x86, do ~/.bashrc doplnil:

export GOROOT=$HOME/go
export GOARCH=386
export GOOS=linux
export PATH=$PATH:$GOROOT/bin

Získáme zdrojové kódy

$ hg clone https://go.googlecode.com/hg/ $GOROOT

A zkompilujeme

$ cd $GOROOT/src
$ ./all.bash
...
--- cd ../test
N known bugs; 0 unexpected bugs

Místo výpustku (…) uvidíte hlášky o kompilaci. N na posledním řádku se bude lišit podle toho, jaké vydání jste zrovna zkompilovali.

Hello, world!

To by k instalaci bylo všechno. Nyní se konečně dostaneme k tomu, abychom napsali první program. Nebude to nic jiného, než snad u každého jazyka obligátní Hello, world!. Takže nalaďte své textové editory na vlny UTF-8 (protože vše napsané v Go je povinně v UTF-8) a tady to je:

package main

import "fmt"

func main() {
    fmt.Println("Hello, world!")
}

Jak tohle zkompilovat pomocí dříve sestaveného kompilátoru? Uložte kód do souboru hello.go, přejděte do adresáře s tímto souborem a další kroky závisí na tom, na jaké jste zrovna architektuře (proměnná $GOARCH). Pro 386 si zapamatujte magické číslo 8, pro amd64 je to 6 a u arm 5. Volím si tebe,386:

$ 8g hello.go
$ 8l -o hello hello.8
$ ./hello
Hello, world!

8g je kompilátor, 8l linker.

Nyní si rozeberme program samotný.

package main

Jako první musí být deklarace balíčku, do kterého soubor patří. Na rozdíl od některých jazyků na vás Go neklade žádné nároky na souvislost mezi názvem balíčku a souboru.

import "fmt"

Následuje seznam balíčků, které chceme importovat. V tomto případě se jedná o velice jednoduchý import – chceme pouze balíček fmt. import však může být i rozmanitější, jak se přesvědčíme později.

func main() {
    fmt.Println("Hello, world!")
}

A nakonec již definice funkce main. Ta dělá jediné – zavolá funkci Println z importovaného balíčku fmt a jako jediný argument jí předá řetězec Hello, world!. Println vytiskne předanou hodnotu a potom odřádkuje.

Tohle byla ukázka jednoho balíčku. Ale program se může skládat z nekonečně mnoha balíčků. Stejně jako jiné kompilované jazyky, i Go má jasně daný tzv. „vstupní bod“ (entry point) – tj. funkce, která se zavolá jako první po spuštění výsledného zkompilovaného programu. Pro Go je touto funkcí main z balíčku main.

Zahrajme si ping pong

Jelikož hned na úvodní stránce webu jazyka je, že cílí také na konkurenční programování, k vysvětlení základů Go poslouží příklad s „ping pongem“ (dvě současně běžící funkce si budou posílat míček; vždy jedna pošle, druhá odehraje zpět).

Aby to nebylo tak jednoduché, nevystačíme si tentokrát s jedním balíčkem. Ne. Budou hned dva! Co kdyby se nám mohlo podobné odehrávání míčku někdy hodit… Na druhou stranu nebudeme věc komplikovat a to, co se stalo s míčkem, prostě vypíšeme do určeného výstupu.

Okénko: balíčky

Program v Go se skládá z balíčků. To bylo vidět už na Hello, world!-u výše, kde byl použit balíček main. Na začátku každého souboru musí být uvedeno, do kterého balíčku patří. Slouží k tomu direktiva package následovaná názvem balíčku, který se musí začínat znakem, který je v Unicode považován za písmeno, nebo podtržítkem (_) a dále může obsahovat další písmena, čísla, nebo podtržítka. Příklad viz Hello, world!.

Konvence

V Go je konvencí, že názvy balíčků jsou psané celé malými písmeny a jsou jednoslovné – v případě, že opravdu cítíte potřebu, že název vašeho balíčku by zrovna musel být víceslovný, zamyslete se ještě jednou. Hlavně by název balíčku měl být krátký, stručný a měl by vypovídat o tom, oč v balíčku jde. Jako inspirace ve stylu pojmenování balíčků může sloužit „standardní“ knihovna.

Balíček se může skládat z více souborů a při pojmenování samotných souborů a podadresářů, kde jsou umístěny, není vyžadované se držet nějakého pevného pravidla. Opět konvence je, že název balíčku se odvíjí od adresáře, ve kterém se nacházejí zdrojové kódy balíčku. Např. balíček v src/pkg/container/vector se importuje jako container/vector a je pojmenován vector.

Kompilace

Vezměme si příklad z toho, jak se kompilují knihovny:

$ 8g -o _go_.8 prvni_soubor.go druhy_soubor.go treti_soubor.go
$ gopack grc balicek.a _go_.8

První příkaz ze souborů prvni_soubor.go, druhy_soubor.go a treti_soubor.go (všechny musí obsahovat na začátku stejnou deklaraci balíčku!) vytvoří soubor _go_.8 (za přepínačem -o následuje název výstupního souboru), který obsahuje binární podobu kódu, tedy zkompilovaný kód, a k tomu ještě různé další metainformace.

Druhý příkaz – gopack – zkompilované soubory (jen _go_.8, ale může jich být i více) zabalí do balíčku (tady balicek.a).

Zautomatizujme to!

Abychom se nemuseli lopotit s kompilováním jednotlivých balíčků ručně, napíšeme Makefile, který to udělá za nás:

TARGET=$(notdir $(shell pwd))
MAGIC=$(shell (([ "$(GOARCH)" == "amd64" ] && echo 6) || ([ "$(GOARCH)" == "arm" ] && echo 5)) || echo 8)

all:
        @for dir in `find * -type d`; do                                        \
            go=$$(find "$$dir" -maxdepth 1 -name "*.go");                       \
            if [ ! -z "$$go" ]; then                                            \
                echo "- $$dir: $$go";                                           \
                $(MAGIC)g -I. -o _go_.8 $$go;                                   \
                rm -f $$dir.a;                                                  \
                gopack grc $$dir.a _go_.8;                                      \
            fi                                                                  \
        done
        @if [ -f 'main.go' ]; then                                              \
            $(MAGIC)g -I. -o _go_.8 main.go;                                    \
            $(MAGIC)l -o $(TARGET) _go_.8;                                      \
        fi
        @rm -f _go_.8
clean:
        @rm -f _go_.8 `find * -name "*.a"` $(TARGET)

Pozn.: Je to testované v GNU Make a nevím, jak je to s kompatibilitou různých příkazů a ostatními implementacemi make.

Princip je takový, že se najdou všechny podadresáře toho pracovního, otestuje se, jestli obsahují nějaké .go soubory, a je-li tomu tak, ze souborů v jednotlivých adresářích se vyrobí balíček. Obsahuje-li pracovní adresář soubor main.go, ten se zkompiluje a slinkuje, čímž se vytvoří spustitelný program nazvaný stejně jako pracovní adresář.

Implementace: balíček pong

Postupně si skrze jednotlivá okénka probereme implementaci balíčku pong. je jednom souboru. Veškerý kód týkající se balíčku pong umístíme do jednoho souboru. Jako každý soubor, i tento musí začínat deklarací balíčku:

package pong

Okénko: import/export

import (
    "fmt";
    "io";
)

Jako v příkladu s Hello, world!-em, i tady potřebujeme naimportovat další balíčky poskytující určitou funkcionalitu. Avšak import tady vypadá jinak, že? Pokud importujeme pouze jeden balíček, můžeme použít zkrácenou formu (import "nazev_balicku"); jestliže jich chceme více, přidáme kulaté závorky a jednotlivé balíčky oddělíme středníkem (středník je pouze oddělovač, za "io" by již být nemusel).

Kdyby se nám nelíbilo jméno balíčku, jaké mu dal jeho autor, můžeme ho přejmenovat:

import (
    bububu "fmt";
    chacha "io";
)

Nebo se dají všechno exportované z balíčku naimportovat do jmenného prostoru tohoto souboru (takže už poté není potřeba psát název balíčku před volané funkce apod.):

import (
    . "fmt";
)

Teď už by se nevolalo fmt.Println(...);, ale Println(...);. Osobně bych se tomuto ale vyhnul – míchat jmenné prostory balíčků není moc dobré.

S importem souvisí i export. Pro export není žádná další klauzule, nýbrž všechno je založené na velikosti počátečního písmena názvu funkce/typu/… – když je na začátku velké písmeno, je název exportován (v balíčku pong je to např. funkce New).

Okénko: typový systém

type Unit interface{}

type Pong struct {
    n int;
    out io.Writer;
    ch chan chan Unit;
    donech chan Unit;
}

type je klauzule vytvářejicí nový typ, který má dané jméno a jehož definice je stejná jako definice jiného typu. Jméno nově vytvářeného typu se dává hned za klauzuli type. Je to nějaký identifikátor a stejně jako pro všechno jiné pro něj platí, že pokud začíná velkým písmenem, je z balíčku exportován. Po jménu typu následuje jeho definice.

Go poskytuje různé předdefinované typy – např. pro celá čísla, čísla s plovoucí desetinnou čárkou, booleovský typ, řetězce atp.

V kódu výše jsou definovány dva typy – Unit a Pong.

Interfaces

Pokud jste zavadili o Javu atp., jistě budete vědět, co to interface je. V Go je to podobné. Interface je abstraktní typ, který definuje, jaké funkce mohou být volány nad typy, které interface tzv. „implementují“ (zavazují se, že budou poskytovat všechno, co definuje interface).

Např. balíček io definuje typ Writer takto:

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

Jelikož v Go se interface implementuje implicitně (neboli kompilátor si interface typu odvodí podle toho, jaké jdou nad typem volat funkce; nemusíme tedy nikde psát „typ blabla implementuje blablabla“), všechno, nad čím lze zavolat funkci Write, implementuje interface Writer (ostatní věci jako seznam parametrů a návratové hodnoty si vysvětlíme později).

Co tedy znamená to interface{}? To je prázdný interface – nevyžaduje, aby nad typem mohla být zavolána jakákoli funkce. Tento interface splňují všechny typy. K čemu je to dobré? U funkcionálních programovacích jazyků je velice často přítomen tzv. „unit“ typ, což je typ, který má právě jednu hodnotu, a v Go bohužel chybí. (Typ Unit bude dále v programu použit pro jednoúčelový signalizační kanál.)

Struktury

Struktura je datový typ, která sdružuje jiné datové typy do jednoho celku. Struktury se skládají z členů, kdy každý člen má své jméno a svůj typ. Struktura Pong má 4 členy – n, out, ch a donech; n je typu int (celočíselný typ), out typu io.Writer (což je interface z balíku io, viz výše), ch je chan chan Unit (kanál, kterým se posílají kanály pro Unit typ; o kanálech později) a donech má typ chan Unit. Pong bude držet informace o hrací ploše, stole na ping pong.

Není to obráceně?

Jestliže jste někdy jen zavadili o Céčkoidní jazyk, asi vás hned napadlo, že ty typové deklarace jsou nějak obráceně. Ano, v Go je to tak – typová deklarace se uvádí za název proměnné, člen struktury, parametr funkce apod. Nejdříve se mi to zdálo velice zběsilé a že se na to nedá zvyknout. Ovšem ony deklarace v Go mají svou logiku.

Okénko: konstanty

const UNIT Unit = 0

Na stránkách Go je hezky řečeno, že konstanty jsou přesně to, co jsou – konstantní. Jsou to pojmenované hodnoty vytvořené při kompilaci, kdy se ve výstupním binárním programu výskyty jména proměnné nahradí za její hodnotu. Konstanty mohou být typované, nebo ne.

Konstanta UNIT je typovaná (typu Unit) a má hodnotu 0. Bude se používat v signalizačních kanálech jako jediný možný typ posílané zprávy.

Okénko: funkce

func New(n int, out io.Writer) *Pong {
    return &Pong{
        n: n,
        out: out,
        ch: make(chan chan Unit),
        donech: make(chan Unit)
    };
}

Funkce jsou nedílnou součástí Go. Jejich definice začínají klíčovým slovem func, kteréžto následuje jméno funkce, v těsném závěsu v kulatých závorkách výčet parametrů a za nimi návratový typ funkce. Po hlavičce je zde tělo, které je ohraničeno složenými závorkami. Go nepodporuje overloading funkcí, tzn. že nejde ve stejném balíčku mít třeba funkce foo(bar int) a foo(baz float, bar string).

New má dva parametry – n typu int a out typu io.Writer – a vrací *Pong, což je ukazatel na typ Pong (definovaný výše). Tělo New je jednoduché – vrací ukazatel na nově alokovanou strukturu Pong. V příkladu s ping pongem bude New vytvářet hrací plochu, stůl, kde si pak budeme pinkat.

Okénko: alokace

Strukturu můžeme alokovat více způsoby. Můžeme využít zabudované funkce new a postupně ji naplnit (stejně se dají alokovat i ostatní typy):

val p *Pong = new(Pong);
p.n = n;
p.out = out;
p.ch = make(chan chan Unit);
p.donech = make(chan Unit);

Nebo strukturového literálu:

val o Pong = Pong{n, out, ch, donech};  // nevrací ukazatel
val p *Pong = &o;                       // získáme adresu

Jelikož já osobně si nikdy nezapamatuji, jak jdou vlastně členové struktury za sebou (hlavně pokud by se mělo jednat o nějakou větší strukturu), rád využiji možnost si pomoci jmény členů (viz funkce New výše).

Někoho možná zarazí, že New vrací při použití strukturního literálu strukturu alokovanou lokálně. To je v Go, jelikož všechny struktury jsou alokovány na haldě a jsou předmětem garbage collection, naprosto v pořádku. V případě, že je alokované místo jakýmkoli způsobem referencováno, místo je stále alokované a neexistuje tedy možnost vzniku „dangling pointeru“.

Kromě new je tu ještě make, která také alokuje místo, ale krom toho provede s ohledem na typ ještě potřebné úkony, abychom s vrácenou hodnotou mohli pracovat – make nevrací ukazatel ale hodnotu. Jedním z typů, který se vytváří pomocí make jsou kanály.

Okénko: metody

func (p *Pong) Start(wait bool) (ok bool, donech chan Unit) {
    go pong(p.ch, p.out);
    go ping(p.n, p.ch, p.out, p.donech);

    if wait {
        <-p.donech;
        return true, nil;
    }

    return true, p.donech;
}

Leč Go nezná nic jako objekt (pokud tedy za objekt nebudeme považovat všechno), ani třídní objektově-orientované programování (v Go žádné třídní hierarchie nenajdete), i tak je tu možnost, jak volat metody nad typy. Metody se od funkcí liší tak, že mezi klíčovým slovem func a názvem metody se uvádí v kulatých závorkách ještě explicitní „this“ – název a typ hodnoty (říkejme tomu třeba objekt), nad kterou může být metoda zavolána.

Právě tímto způsobem se implementují interfaces.

Můžete vidět, že metoda Start je metoda pro typ *Pong (ukazatel na Pong) přijímá jeden parametr – wait – a má dvě návratové hodnoty. Ano, funkce a metody v Go mohou vracet více hodnot. Také si můžeme návratové hodnoty pojmenovat (viz příklad) a přistupovat k nim v těle funkce/metody. Tady toho není využito, ovšem pojmenovat je i přesto je doporučeníhodný postup, protože to dokumentuje, o co se jedná, je to výmluvnější než nějaké (bool, chan Unit).

Tělo Start se skládá ze dvou částí – spuštění goroutines a počkání na to, až doběhnou, je-li wait == true, nebo navrácení kanálu, po kterém přijde signál, že dříve spuštěné goroutines dokončily svou činnost, když je wait == false.

Okénko: goroutines & kanály

Goroutines jsou způsob, jak paralelizovat kód v Go. Goroutine se může stát jakákoli funkce, která má prázdný návratový typ a to tak, že při volání této funkce před ni přidáme klíčové slovo go. Runtime jazyka se postará o vytvoření všeho potřebného (např. namapování předaných parametrů na zásobník nového vlákna). S goroutines nešetřete, vše je optimalizované tak, že jich bez mrknutí oka můžete vytvořit 100 tisíc a nic se nestane.

Kanály jsou způsobem, jak spolu goroutines mohou komunikovat. Na sdílení paměti mezi vlákny zapomeňte, tohle jsou goroutines, ty komunikují posíláním zpráv. Motto goroutines je: „Nekomunikujte sdílením dat, sdílejte data komunikací.“

Okénko: přiřazení

Operátory = a := v Go mohou lidem trochu zamotat hlavu. Jak to s nimi vlastně je? = stejně jako v Céčkoidních jazycích je operátor přiřazení, tzn. že hodnotu uloží do proměnné. A jelikož je Go staticky silně typovaný jazyk, každá proměnná musí být deklarováná:

var foo int;

Toto deklarovalo proměnnou foo typu int. Nyní jí můžeme vesele přiřazovat hodnoty:

foo = 1;
foo = 2;
foo = 3;

Operátor := tyhle dva kroky dokáže smrsknout do jednoho – podle návratového typu výrazu napravo od operátoru odvodí typ proměnné, „deklaruje“ ji a přiřadí do ní hodnotu vyhodnoceného výrazu. Takže např. bez jakékoli deklarace se dá vytvořit proměnná bar typu int takto:

bar := 1;

Ovšem teď je proměnná vytvořena, takže jakékoli další přiřazování zajištuje již operátor =:

bar = 2;
bar = 3;

Implementace: goroutine pong

func pong(ch chan chan Unit, out io.Writer) {
    fmt.Fprintln(out, "pong started");

    for {
        if pingch := <-ch; pingch != nil {
            fmt.Fprintln(out, "pong received ping");
            pingch <- UNIT;
        } else { break; }
    }

    fmt.Fprintln(out, "pong finished");
}

pong v našem ping pongu komunikuje s druhou spouštěnou goroutine ping pomocí kanálu, skrz který se posílají kanály.

Na začátku se pong ohlásí, že je v provozu tím, že zapíše řetězec pomocí funkce fmt.Fprintln někam, kam ukazuje out.

Dále se dostaneme do nekonečné smyčky for – něco jako for(;;), nebo while(true) z jiných Céčkoidních jazyků. if v tomto příkladě je trochu speciální – if v Go umožňuje, abychom nejdříve provedli nějakou incializaci (pingch := <-ch) a poté teprve testovali podmínku (pingch != nil).

Pokud nám přijde nějaký smysluplný kanál, tak na něj odpovíme, jinak je konec komunikace, pomocí break vyskočíme ze smyčky, ohlásíme, že pong skončil, a to je konec těla funkce, goroutine zanikne a runtime se postará o uvolnění věcí s ní spojených.

Implementace: goroutine ping

func ping(n int, ch chan chan Unit, out io.Writer, donech chan Unit) {
    fmt.Fprintln(out, "ping started");

    for ; n > 0; n-- {
        pingch := make(chan Unit);
        ch <- pingch;
        <-pingch;
        fmt.Fprintln(out, "ping received pong");
    }

    ch <- nil;

    fmt.Fprintln(out, "ping finished");
    donech <- UNIT;
}

ping n-krát vytvoří kanál, který pošle pongU a na kterém bude čekat od pongU odpověď. Tady je už takové klasičtější for – hlavička má tři části oddělené středníky: incializace; podmínka; v každé iteraci se po těle provede tohle.

Pak ještě ukončíme pong tak, že mu zašleme místo očekávaného kanálu nil a signalizujeme na kanálu donech, že je po všem.

Tohle je všechno, co v balíčku pong najdeme.

Implementace: balíček main

Obsah souboru main.go:

package main

import (
    "fmt";
    "os";
    "flag";
    "pong";
)

var count = flag.Int("c", 3, "count of pings");
var help = flag.Bool("h", false, "show help");
func init() { flag.Parse(); }

func main() {
    if *help {
        flag.PrintDefaults();
        return;
    }

    pong.New(*count, os.Stdout).Start(true);
    fmt.Println("That's all folks!");
}

Jako obvykle soubor začíná definicí balíčku, ke kterému náleží. import je doufám také jasný.

var count = flag.Int("c", 3, "count of pings");
var help = flag.Bool("h", false, "show help");
func init() { flag.Parse(); }

Aby se dal výsledný program nějak ovládat, toto nadefinuje některé parametry příkazové řádky pomocí nástrojů z balíčku flag. Nejzajímavější je asi funkce init, která pokud je v balíčku, se spustí vždy na začátku programu (každý balíček může mít svůj init, kterým můžeme připravit potřebné věci).

func main() {
    if *help {
        flag.PrintDefaults();
        return;
    }

    pong.New(*count, os.Stdout).Start(true);
    fmt.Println("That's all folks!");
}

main ověří, jestli nechceme zkouknout nápovědu a jestli ano, ukáže nám ji. Jinak zavolá funkci New z balíčku pong, která vrátí nově vytvořenou hrací plochu, nad níž si spustíme metodu Start a necháme Start ať počká, až goroutines doběhnou.

Když program zkompilujeme a pustíme:

$ ./pingpong
pong started
ping started
pong received ping
ping received pong
pong received ping
ping received pong
pong received ping
ping received pong
ping finished
pong finished
That's all folks!

Kritika

Kolem Go je velká kontroverze. Je to od Google, je to nahypované, někteří jazyk zbožňují, jiní ho nenávidí. Tato sekce by se měla věnovat pár otázkám, které se kolem Go vyrojily.

Další jazyk?

Jak tvrdí sami autoři, Go je prostě experiment. První reakcí jednoho člověka, který se ode mne dozvěděl, že v laboratořích Google „vymysleli“ nový jazyk bylo: „Bože!“ Každý má svůj oblíbený jazyk, kterého se bude držet, který bude obhajovat, pro který bude vést svaté války.

Jedna z otázek ve FAQ je, proč prostě nenapsat nějakou knihovnu pro C/C++, skrz kterou by se komunikovalo. Odpověď je taková, že C/C++ mají hodně problémů, které jsou zakořeněny přímo v návrhu jazyka, a jim se prostě nejde vyvarovat jinak než tím, jak to to udělali s Go.

Obráceně?

Hodně lidem vadí, že typové deklarace jsou až za názvem proměnných, návratový typ funkce až za seznamem parametrů apod. Jak již bylo řečeno, deklarace v Go mají svou logiku.

Žádné OOP?

A jak to, že Go nepodporuje objektově-orientované programování?! OOP je docela široké, takže specifikujme, že většině lidem vadí, že Go nepodporuje právě ten typ OOP, na který jsou zvyklí, tedy třídní OOP ve stylu C++, Javy atd. Je to pravda, nikdo to nezapírá.

Ale musím se stylu, jaké zvolilo Go, zastat. OOP mělo udělat programování jednodušším, ale podle mě akorát přineslo hromadu termínů, jakýchsi „vzorů“ a vůbec dalšího balastu. Rigidně dané třídní hierarchie jsou velice svazující. Jen v málo případech najdete takovou věc, pro kterou by se takováto hierarchie hodila. V mnoha úvodech k OOP se uvádí jako takový typický příklad, jelikož OOP má sloužit k modelování reálného světa, biologický taxonomický systém. Avšak dnes s trendem, který v této taxonomii nastává, je to příklad dosti nepřesný, ne-li úplně špatný (alespoň podle mě). Odedávna bylo několik nejhlavnějších skupin – říší; např. živočichové, rostliny atd. A některé výzkumy zjistily, že organismy (hlavně se jedná o mikroorganismy), které se řadily mezi živočichy, nebo rostliny, mohou v určitý podmínkách úplně změnit svůj způsob života. Příkladem jsou krásnoočka, která mohou fotosyntetizovat (vytvářet z oxidu uhličitého a slunečního záření energii a kyslík), což by je řadilo mezi rostliny, ale když jsou špatné světelné podmínky, chloroplasty (které provádějí fotosyntézu) se rozpustí a krásnoočko začne žít dravým způsobem života (energii získává pojídáním ostatních organismů); to je zase většinou výsadou živočichů. Jak potom chcete model programávání, který má reflektovat skutečný svět, stavět na pevně daných hierarchiích, když v reálu to tak není?

Jako příklad, proč je dobré se nedržet třídního OPP, jsem si připravil takový jednoduchý HTTP server, který vrací aktuální čas:

package main

import (
    "http";
    "io";
    "time";
    "flag";
    "strconv";
)

var port = flag.Int("p", 1234, "port to listen to");
func init() { flag.Parse(); }

func main() {
    mux := http.NewServeMux();
    mux.Handle("/", http.HandlerFunc(func (c *http.Conn, r *http.Request) {
        now := time.LocalTime();
        io.WriteString(c, now.Asctime());
    }));

    err := http.ListenAndServe(":" + strconv.Itoa(*port), mux);
    if err != nil { panic("ListenAndServe:", err.String()); }
}

Malinký jednosouborový server. Pokud ho chcete zkompilovat, můžete využít již použitý Makefile: vytvořte nový adresář s názvem httpdate, do něj uložte dříve použitý Makefile a poté kód výše jako soubor main.go. Zkompilujte pomocí make:

$ make

Když spustíte a nenastane žádná chyba:

$ ./httpdate

Otevřete si v prohlížeči adresu http://localhost:1234/, uvidíte aktuální čas.

Nejdůležitější částí je:

mux.Handle("/", http.HandlerFunc(func (c *http.Conn, r *http.Request) {
    now := time.LocalTime();
    io.WriteString(c, now.Asctime());
}));

Metoda Handle typu ServeMux přijímá dva argumenty – řetězec, URI, na kterou se druhý parametr typu Handler napojí. Handler bude obsluhovat požadavky, které přijdou s URI prvního parametru.

Handler je velice jednoduchý interface:

type Handler interface {
    ServeHTTP(*Conn, *Request);
}

Díky typovému systému Go může být takovým Handlerem cokoli. Tady je to např. anonymní funkce, tzv. closure, přetypovaná na typ HandlerFunc z balíku http. Typ HandlerFunc:

type HandlerFunc func(*Conn, *Request)

má nadefinovanu totiž metodu ServeHTTP takto:

func (f HandlerFunc) ServeHTTP(c *Conn, req *Request) { f(c, req) }

Všechno je to typově korektní a kontrolované v čase kompilace. Ale stejně tak si mohu nadefinovat podobnou metodu pro řetězec:

type HandlerString string
func (s HandlerString) ServeHTTP(c *http.Conn, req *http.Request) {
    io.WriteString(c, string(s));
}

A potom jen předáte muxu řetězec jako druhý parametr a je to, na /hello servírujete tento řetězec:

mux.Handle("/hello", HandlerString("hello, world!"));

Toto je síla, o které se může jazykům s rigidní hierarchickou strukturou tříd jen snít.

Fault-tolerancy ne-e?

Co mně osobně vadí je, že se Go nevydalo cestou více za fault-tolerancy. Co to vůbec „fault-tolerancy“ je? Svět není ideálem, jakým bychom ho chtěli mít, a tak se stane, že v počítači odejde disk, shoří zdroj, cokoli se porouchá a hardware je na vyhození. Hlavně pokud se jedná o cloud computing, toto je scénář, se kterým by se mělo počítat. Ovšem chyba může být i v programu – člověk udělá nějakou botu, něco opomene a třeba nastane deadlock. A teď nastává problém, jak psát programy tak, aby se s tímhle dokázaly co nejlépe vyrovnat. „Fault-tolerancy“ je míra, s jakou se s tím programy vyrovnat dokáží.

Jediným jazykem, o kterém vím, že podobné mechanismy má zabudované přímo v sobě, je Erlang. Toto je článek o Go, takže se o Erlangu nebudu rozepisovat, pouze odkážu na Wikipedii. Erlang byl vytvořen s tím, aby pro něj selhání hardware ani programů nebyl žádný problém. Spolehlivé systémy se většinou řeší tak, že v programu je jeden i více supervizorů, kteří dohlížejí na ostatní části programu, a když se něco stane, restartují daný modul, nebo provedou nějaký jiný úkol. Taky je možnost mít tyto supervizory na jiných fyzických počítačích a tím se řeší i hardwarové problémy – když jeden počítač odejde, supervizor nastartuje vše potřebné někde jinde. Spouštět kód na jiných kusech hardware, to goroutines bohužel neumí.

Závěr

Go se mi velice líbí (myslím, že to v celém článku bylo poznat). Má chyby, ne že ne. Avšak když se na to podívám s větším odstupem, je to jazyk, který těch chybiček má asi nejméně z těch jazyků, které znám. Idea typového systému a metod je v podání Go podle mě brilantní. Co mi chybí je „unit“ typ, který by se pro signalizační kanály opravdu hodil (v oficiálních tutoriálech na golang.org je používán pro tyto kanály typ bool; ale ten je přeci na něco jiného!). Vzdálené spouštění goroutines by velmi usnadnilo práci; nyní se to bude muset řešit ručně. Mé postavení k typovým deklaracím je neutrální – ani to neoslavuji, ani nezatracuji; upřímně mi je docela jedno, jestli ten int napíšu za nebo před foo. Ještě by mohlo být lépe vyřešené alokování a inicializace kanálů, hashmap apod. – zabudovaná funkce make se mi příliš nelíbí.

Zdrojové kódy příkladů (na stažení)

Zdroje (aneb také stojí za přečtení)

×Odeslání článku na tvůj Kindle

Zadej svůj Kindle e-mail a my ti pošleme článek na tvůj Kindle.
Musíš mít povolený příjem obsahu do svého Kindle z naší e-mailové adresy kindle@programujte.com.

E-mailová adresa (např. novak@kindle.com):

TIP: Pokud chceš dostávat naše články každé ráno do svého Kindle, koukni do sekce Články do Kindle.

Hlasování bylo ukončeno    
0 hlasů
Google
(fotka) Jakub KulhanAutor momentálně studuje na osmiletém gymnáziu v Kralupech nad Vltavou. Programování se věnuje od 11 let, kdy ho poprvé uchvátila možnost "mít vlastní stránky". Nakrátko poté objevil PHP a už se to s ním "vezlo". Webové aplikace zůstaly jeho hlavní doménou, ale ve svém volném čase probádává nejrůznější zákoutí světa programování, programovacích jazyků a všeho kolem nich.
Web    

Nové články

Obrázek ke článku Stavebnice umělé inteligence 1

Stavebnice umělé inteligence 1

Článek popisuje první část stavebnice umělé inteligence. Obsahuje lineární a plošnou optimalizaci.  Demo verzi je možné použít pro výuku i zájmovou činnost. Profesionální verze je určena pro vývojáře, kteří chtějí integrovat popsané moduly do svých systémů.

Obrázek ke článku Hybridní inteligentní systémy 2

Hybridní inteligentní systémy 2

V technické praxi využíváme často kombinaci různých disciplín umělé inteligence a klasických výpočtů. Takovým systémům říkáme hybridní systémy. V tomto článku se zmíním o určitém typu hybridního systému, který je užitečný ve velmi složitých výrobních procesech.

Obrázek ke článku Jak vést kvalitně tým v IT oboru: Naprogramujte si ty správné manažerské kvality

Jak vést kvalitně tým v IT oboru: Naprogramujte si ty správné manažerské kvality

Vedení týmu v oboru informačních technologií se nijak zvlášť neliší od jiných oborů. Přesto však IT manažeři čelí výzvě v podobě velmi rychlého rozvoje a tím i rostoucími nároky na své lidi. Udržet pozornost, motivaci a efektivitu týmu vyžaduje opravdu pevné manažerské základy a zároveň otevřenost a flexibilitu pro stále nové výzvy.

Obrázek ke článku Síla týmů se na home office může vytrácet. Odborníci radí, jak z pracovních omezení vytěžit maximum

Síla týmů se na home office může vytrácet. Odborníci radí, jak z pracovních omezení vytěžit maximum

Za poslední rok se podoba práce zaměstnanců změnila k nepoznání. Především plošné zavedení home office, které mělo být zpočátku jen dočasným opatřením, je pro mnohé už více než rok každodenní realitou. Co ale dělat, když se při práci z domova ztrácí motivace, zaměstnanci přestávají komunikovat a dříve fungující tým se rozpadá na skupinu solitérů? Odborníci na personalistiku dali dohromady několik rad, jak udržet tým v chodu, i když pracovní podmínky nejsou ideální.

Hostujeme u Českého hostingu       ISSN 1801-1586       ⇡ Nahoru Webtea.cz logo © 20032024 Programujte.com
Zasadilo a pěstuje Webtea.cz, šéfredaktor Lukáš Churý