Hlavní navigace

Vývoj síťových aplikací v programovacím jazyku Go

Pavel Tišnovský

Jednou z oblastí, v níž je jazyk Go úspěšně používán, jsou síťové aplikace, ať již se jedná o jednoúčelové utility, nebo i o mnohdy komplikované servery. V dnešním článku se budeme zabývat právě tímto důležitým tématem.

Doba čtení: 31 minut

11. Zobrazení hlaviček vrácených v odpovědi HTTP serveru

12. Vytvoření jednoduchého HTTP serveru

13. Server s měnitelným stavem a problémy, které to způsobí

14. Role mutexů v programovacím jazyce Go

15. Úprava HTTP serveru takovým způsobem, aby používal mutexy

16. HTTP server nabízející statický obsah (soubory)

17. Kombinovaný HTTP server poskytující statický i dynamický obsah

18. Repositář s demonstračními příklady

19. Odkazy na Internetu

1. Vývoj síťových aplikací v programovacím jazyku Go

Již v úvodním článku seriálu o programovacím jazyku Go jsme si řekli, že po představení stabilní verze Go nastala poměrně zajímavá a vlastně i neočekávaná situace – tvůrci Go totiž předvídali spíše přechod programátorů používajících jazyky C a C++ ke Go, ovšem programovací jazyk Go a jeho možnosti objevili programátoři, kteří psali své síťové a speciálně pak webové aplikace v Pythonu, Ruby či Java/TypeScriptu (Node.js). Přepis do jazyka Go mnohdy znamenal řádový a někdy i dvouřádový (100×) nárůst výkonu těchto aplikací. Do jisté míry je nárůst výkonu způsoben překladem do nativního kódu, ovšem nesmíme zapomenout na gorutiny, které nejsou (na rozdíl od klasických vláken) příliš náročné na paměť, takže se můžeme setkat s aplikacemi, v nichž bez větších problémů běží stovky či dokonce tisíce gorutin (ostatně benchmark z předchozího článku vytvořil až 4097 gorutin, a to bez jakéhokoli viditelného problému).

Dnes se seznámíme se základními balíčky určenými právě pro tvorbu síťových aplikací – jednoduchých a jednorázových utilit, ale například i HTTP serverů (což je užitečné téma, kterému se budeme věnovat od dvanácté kapitoly). Samozřejmě se nebude jednat pouze o popis balíčků určených pouze pro práci se síťovými protokoly, protože typické síťové aplikace vyžadují i další funkcionalitu – zpracování formátů JSON a XML (čtení i zápis), kódování Base64, kryptografické algoritmy atd. atd.

Poznámka: vzhledem k tomu, že při výchozím nastavení překladače Go se vytváří binární soubory obsahující i všechny potřebné knihovní funkce, budou výsledky dnešních příkladů objemnější, než jsme byli doposud zvyklí. Zatímco program typu „Hello world“ má po překladu a slinkování velikost cca 1MB na platformě x86–64, je tomu u dnešních příkladů jinak: cca 2,5 MB u jednoduchých klientů a serverů a přes 6 MB u plnohodnotného HTTP serveru. Velikost lze samozřejmě snížit, nejjednodušeji utilitkou strip, dále pak upx atd.

2. Jednoduchý klient přijímající libovolnou sekvenci bajtů

Síťové aplikace se vytváří na různých úrovních abstrakce – buď se pouze otevřou připojení (například s využitím Unix socketů) a následný komunikační protokol je naprogramován přímo vývojářem, nebo se naopak využije nějaký již existující protokol na vyšší síťové vrstvě (HTTP atd.). Nejprve si ukážeme komunikaci mezi jednoduchým klientem a serverem na nižší úrovni, kdy náš komunikační protokol (na aplikační úrovni) bude spočívat v přenosu jediného bajtu přes TCP, popř. přes UDP.

Poznámka: na nižších síťových vrstvách (TCP, IP, Ethernet) bude samozřejmě komunikace složitější, od toho ovšem budeme odstíněni standardními knihovnami (balíčky) programovacího jazyka Go.

Samotná implementace klienta bude poměrně přímočará a bude se skládat z těchto kroků:

  1. Navázání připojení s využitím konstruktoru net.Dial(protokol, adresa), který je popsán na adrese https://golang.org/pkg/net/#Dial. Použitý protokol je specifikován řetězcem; konkrétně se může jednat o konstanty „tcp“, „tcp4“, „tcp6“, „udp“, „udp4“, „udp6“, „ip“, „ip4“, „ip6“, „unix“, „unixgram“ popř. „unixpacket“. V příkladu zvolíme „tcp“, který bude funkční v sítích s IPv4 i IPv6 (nebo pochopitelně v kombinovaných sítích).
  2. Přečtení n bajtů metodou Read(b []byte) (n int, err error) (konkrétní příjemce se liší podle toho, jaké připojení jsme deklarovali v konstruktoru, ovšem tato metoda bude vždy podporována). Povšimněte si, že této metodě je nutné předat řez (slice) a nikoli pole bajtů (to je nekompatibilní datový typ). Z tohoto důvodu v našem příkladu použijeme trik pole[:], kterým se vytvoří řez ukazující na celé pole (přesněji řez bude ukazovat na první prvek pole a jeho délka bude odpovídat délce pole).
  3. Přečtené pole bajtů se následně vytiskne, přičemž server implementovaný v rámci další kapitoly je naprogramován takovým způsobem, aby posílal jen jediný bajt.

Úplný zdrojový kód tohoto příkladu naleznete na adrese https://github.com/tisnik/go-fedora/blob/master/article11/01_sim­ple_client.go:

package main
 
import (
        "fmt"
        "net"
)
 
func main() {
        conn, err := net.Dial("tcp", "localhost:1234")
        if err != nil {
                println("Connection refused!")
        } else {
                var b [1]byte
                n, err := conn.Read(b[:])
                if err != nil {
                        println("No response!")
                } else {
                        if n == 1 {
                                fmt.Printf("Received %d byte: %v\n", n, b)
                        } else {
                                fmt.Printf("Received %d bytes: %v\n", n, b)
                        }
                }
        }
}

O realizovaném připojení si můžeme zjistit i další informace, například lokální i vzdálenou adresu s využitím metod Conn.LocalAddr() a Conn.RemoteAddr(). Tyto adresy převedeme do tisknutelného tvaru metodou String(). Upravený klient může vypadat následovně:

package main
 
import (
        "fmt"
        "net"
)
 
func main() {
        conn, err := net.Dial("tcp", "localhost:1234")
        if err != nil {
                println("Connection refused!")
        } else {
                fmt.Printf("Connection established\n")
                fmt.Printf("Remote Address: %s \n", conn.RemoteAddr().String())
                fmt.Printf("Local Address:  %s \n", conn.LocalAddr().String())
 
                var b [1]byte
                n, err := conn.Read(b[:])
                if err != nil {
                        println("No response!")
                } else {
                        if n == 1 {
                                fmt.Printf("Received %d byte: %v\n", n, b)
                        } else {
                                fmt.Printf("Received %d bytes: %v\n", n, b)
                        }
                }
        }
}

Příklad výsledku po připojení klienta k serveru popsanému v další kapitole:

Connection established
Remote Address: 127.0.0.1:1234
Local Address:  127.0.0.1:38082
Received 1 byte: [0]
Poznámka: povšimněte si, že server má pevně zadaný port 1234, zatímco port otevřený na straně klienta je zvolen systémem.

3. Server vracející počet realizovaných připojení

Nyní si ukažme implementaci serveru, na který se budou připojovat výše popsaní klienti. Implementace serveru je nepatrně složitější než implementace klienta, a to z toho důvodu, že server musí obsloužit větší množství klientů. V tom nejjednodušším případě použijeme „neforkujícího“ klienta, který bude implementován následujícím způsobem:

  1. Použijeme konstruktor net.Listen(), v němž opět specifikujeme protokol (viz předchozí kapitolu) a síťové rozhraní s portem 1234.
  2. S využitím příkazu defer zajistíme, že se při ukončení funkce main automaticky uzavře i otevřený port.
  3. Dále v nekonečné smyčce budeme čekat na připojení v metoděAccept. Jakmile se nějaký klient pokusí o připojení, vrátí tato metoda strukturu implementující mj. metody Read a Write. A právě s využitím metody Write pošleme klientovi jediný bajt obsahující hodnotu počitadla dotazů.
  4. Spojení se automaticky ukončí díky použití příkazu defer l.Close()

Úplný zdrojový kód tohoto příkladu vypadá následovně:

package main
 
import (
        "net"
)
 
func main() {
        cnt := byte(0)
 
        l, err := net.Listen("tcp", "localhost:1234")
        if err != nil {
                println("Can't open the port!")
        }
        defer l.Close()
        for {
                conn, err := l.Accept()
                defer conn.Close()
                if err != nil {
                        println("Connection refused!")
                } else {
                        var b = []byte{cnt}
                        cnt++
                        conn.Write(b)
                }
        }
}

Prakticky je možné při otevírání portu vynechat část se síťovým rozhraním, takže se skript nepatrně zjednoduší:

package main
 
import (
        "net"
)
 
func main() {
        cnt := byte(0)
 
        l, err := net.Listen("tcp", ":1234")
        if err != nil {
                println("Can't open the port!")
        }
        defer l.Close()
        for {
                conn, err := l.Accept()
                defer conn.Close()
                if err != nil {
                        println("Connection refused!")
                } else {
                        var b = []byte{cnt}
                        cnt++
                        conn.Write(b)
                }
        }
}

Ještě si vyzkoušejme, jak se bude server chovat ve chvíli, kdy bude vyřízení požadavku trvat dlouhou dobu. Upravený zdrojový kód serveru bude vypadat takto:

package main
 
import (
        "net"
        "time"
)
 
func main() {
        cnt := byte(0)
 
        l, err := net.Listen("tcp", "localhost:1234")
        if err != nil {
                println("Can't open the port!")
        }
        defer l.Close()
        for {
                conn, err := l.Accept()
                println("connection accepted")
                time.Sleep(2 * time.Second)
                defer conn.Close()
                if err != nil {
                        println("Connection refused!")
                } else {
                        var b = []byte{cnt}
                        cnt++
                        conn.Write(b)
                }
                println("connection closed")
        }
}

V tomto případě, nezávisle na počtu spuštěných klientů, bude server vždy zpracovávat požadavek od jediného klienta:

connection accepted
connection closed
connection accepted
connection closed
Poznámka: sami si vyzkoušejte chování serveru v případě, že z několika terminálů spustíte více klientů.

4. Server, který dokáže obsloužit větší množství klientů

Kostra předchozího serveru dokázala obsloužit v daný okamžik pouze jediného klienta. Je tomu tak z toho důvodu, že metoda l.Accept() skončila ve chvíli, kdy se k serveru připojí jediný klient a následně se musí ukončit celé tělo smyčky, aby se l.Accept() zavolala znovu:

conn, err := l.Accept()
defer conn.Close()
if err != nil {
        println("Connection refused!")
} else {
        var b = []byte{cnt}
        cnt++
        conn.Write(b)
}

Nic nám ovšem nebrání, aby se vlastní obsluha klienta provedla v gorutině běžící paralelně s hlavní gorutinou. Na obslužnou gorutinu nikde nečekáme, takže se další volání l.Accept() provede velmi rychle:

conn, err := l.Accept()
if err != nil {
        println("Connection refused!")
} else {
        println("Connected")
        go func(c net.Conn) {
                var b = []byte{cnt}
                cnt++
                c.Write(b)
                c.Close()
        }(conn)
}
Poznámka: jak uvidíme v dalším textu, nebude chování serveru zcela korektní, protože zvyšování hodnoty proměnné cnt není atomické.

Podívejme se nyní na úplný zdrojový kód tohoto příkladu:

package main
 
import (
        "net"
)
 
func main() {
        cnt := byte(0)
 
        l, err := net.Listen("tcp", "localhost:1234")
        if err != nil {
                println("Can't open the port!")
        }
        defer l.Close()
        for {
                conn, err := l.Accept()
                if err != nil {
                        println("Connection refused!")
                } else {
                        println("Connected")
                        go func(c net.Conn) {
                                var b = []byte{cnt}
                                cnt++
                                c.Write(b)
                                c.Close()
                        }(conn)
                }
        }
}
Poznámka: opět si vyzkoušejte, jak se bude server chovat při souběžném připojení většího množství klientů.

5. Server posílající textové zprávy

Přenosový protokol implementovaný předchozími verzemi serveru byl skutečně velmi primitivní, proto si nyní ukážeme nepatrně složitější implementaci serveru, který klientovi pošle textový řádek s ukončujícím znakem \n na konci. To je možné zajistit snadno, protože hodnota vrácená funkcí l.Accept() (l je například typ TCPListener, UnixListenr apod., obecným typem je Listener) reprezentuje handle souboru, do něhož je možné zapsat řetězec například funkcí fmt.Fprintf:

fmt.Fprintf(c, "Holla\n")

Po poslání řetězce se spojení ihned ukončí:

c.Close()

Celá obsluha klienta je samozřejmě realizována ve vlastní gorutině:

for {
        conn, err := l.Accept()
        if err != nil {
                println("Connection refused!")
        }
        go func(c net.Conn) {
                fmt.Fprintf(c, "Holla\n")
                c.Close()
        }(conn)
}

Alternativně (což je možná korektnější) by bylo zapsat gorutinu s využitím příkazu defer:

go func(c net.Conn) {
    defer c.Close()
    fmt.Fprintf(c, "Holla\n")
}(conn)
Poznámka: na druhou stranu by však byla následující konstrukce nekorektní, protože by k uzavření připojení mohlo dojít dřív, než by se provedl zápis:
conn, err := l.Accept()
defer conn.close()
if err != nil {
        println("Connection refused!")
}
go func(c net.Conn) {
        fmt.Fprintf(c, "Holla\n")
}(conn)

Zdrojový kód serveru naleznete na adrese https://github.com/tisnik/go-fedora/blob/master/article11/05_tex­t_client.go:

package main
 
import (
        "fmt"
        "net"
)
 
func main() {
        l, err := net.Listen("tcp", "localhost:1234")
        if err != nil {
                println("Can't open the port!")
        }
        defer l.Close()
        for {
                conn, err := l.Accept()
                if err != nil {
                        println("Connection refused!")
                }
                go func(c net.Conn) {
                        fmt.Fprintf(c, "Holla\n")
                        c.Close()
                }(conn)
        }
}

Předností je, že tento server můžeme otestovat s využitím běžného Telnetu:

$ telnet localhost 1234
 
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Holla
Connection closed by foreign host.

6. Klient pracující v textovém režimu

Úprava klienta takovým způsobem, aby akceptoval celý textový řádek popř. větší množství řádků, je nepatrně složitější, než tomu bylo u serveru, protože je nutné nějakým způsobem „rozluštit“ sekvenci bajtů poslaných serverem. Můžeme zde využít například již známý balíček bufio, s nímž jsme se seznámili v předchozím článku v rámci popisu bufferování standardního výstupu. Nyní ovšem použijeme metodu Reader.ReadString(), které se předá znak používaný pro ukončení jednotlivých záznamů. A v našem případě je záznamem celý textový řádek, takže oddělovačem bude znak \n:

status, err := bufio.NewReader(conn).ReadString('\n')

Tato metoda vrátí příznak chyby v případě, že celá sekvence nekončí oddělovacím/ukončovacím znakem.

Podívejme se nyní na úplný zdrojový kód klienta, který naleznete na adrese https://github.com/tisnik/go-fedora/blob/master/article11/06_tex­t_server.go:

package main
 
import (
        "bufio"
        "fmt"
        "net"
)
 
func main() {
        conn, err := net.Dial("tcp", "localhost:1234")
        if err != nil {
                println("Connection refused!")
        } else {
                fmt.Fprintf(conn, "Hello")
                status, err := bufio.NewReader(conn).ReadString('\n')
                println(status, err)
                if err != nil {
                        println("No response!")
                } else {
                        println(status)
                }
        }
}

7. Získání IP adresy pro specifikované hostname

V balíčku net nalezneme kromě dalších užitečných věcí i definici datového typu pojmenovaného příhodně IP. Interně se jedná o řez bajtů, který reprezentuje IP adresu, ať již podle standardu IPv4 nebo IPv6. IP adresy je možné vytisknout čitelným způsobem díky existenci metody String definované právě pro typ IP:

func (ip IP) String() string

Dále se nám může v aplikacích hodit funkce pojmenovaná LookupIP, které se předá řetězec obsahující například doménové jméno a návratovými hodnotami je dvojice: řez s prvky typu IP a příznak chyby. Řez se vrací z toho důvodu, že doménovému jménu může odpovídat více IP adres – typicky jedna IPv4 a jedna IPv6.

Opět si ukažme přímočarý způsob použití funkce LookupIP v následujícím příkladu:

package main
 
import (
        "fmt"
        "net"
)
 
func performLookup(address string) {
        ip, err := net.LookupIP(address)
        if err != nil {
                println("Lookup failure")
        } else {
                fmt.Printf("%v\n", ip)
        }
}
 
func main() {
        performLookup("localhost")
        performLookup("root.cz")
        performLookup("google.com")
}

Po překladu a spuštění tohoto příkladu by se na standardním výstupu měly objevit tyto informace:

[127.0.0.1]
[91.213.160.188 2001:67c:68::76]
[172.217.19.110 2a00:1450:400d:804::200e]
Poznámka: vidíme, že se v posledních dvou příkladech skutečně vrátila dvojice IP adres, která byla naformátována výše zmíněnou metodou String.

8. Další funkce používané v souvislosti s IP adresami

Další užitečnou funkcí z balíčku net je funkce nazvaná ParseIP, které se předá řetězec s různými reprezentacemi IP adresy a výsledkem je hodnota typu IP (řez bajtů). Tato funkce je užitečná především pro IPv6 adresy, které se mnohdy zapisují zkráceným způsobem, například „2041:0000:140F::875B:131B“ namísto „2041:0000:140F:0000:0000­:0000:875B:131B“ (v prvním případě je použita „čtyřtečka“), ve druhém jsou explicitně zapsány všechny octety.

Tuto funkci si vyzkoušíme snadno:

package main
 
import (
        "fmt"
        "net"
)
 
func parseIP(address string) {
        ip := net.ParseIP(address)
        if ip == nil {
                println("ParseIP failure")
        } else {
                fmt.Printf("%v\n", ip)
        }
}
 
func main() {
        parseIP("127.0.0.1")
        parseIP("1000::68")
        parseIP("fe80::224:d7ff:fe83:1f28")
        parseIP("fe80:0000:0000:0000:224:d7ff:fe83:1f28")
        parseIP("fe80:0000:0000:0000:0000:0000:0000:0001")
}

Výsledky by měly vypadat takto:

127.0.0.1
1000::68
fe80::224:d7ff:fe83:1f28
fe80::224:d7ff:fe83:1f28
fe80::1
Poznámka: opětovné zkrácení nám opět zajistila metoda String pro typ IP.

v posledním příkladu tohoto typu je ukázáno použití konstruktoru IPv4, kterému se předá čtveřice bajtů a výsledkem je hodnota typu IP, tj. plnohodnotná IP adresa:

package main
 
import (
        "fmt"
        "net"
)
 
func main() {
        fmt.Printf("%v\n", net.IPv4(127, 0, 0, 1))
        fmt.Printf("%v\n", net.IPv4(192, 168, 10, 3))
}

Příklad výstupu:

127.0.0.1
192.168.10.3
Poznámka: tento konstruktor tedy vlastně interně převádí čtyři samostatně zapsané bajty na pole bajtů.

9. Balíček http

Konečně se dostáváme k popisu balíčku net/http. Jak již název tohoto balíčku napovídá, obsahuje funkce a nové datové typy určené pro práci s protokolem HTTP, a to jak pro klienty, kteří posílají dotazy (request) na servery, tak i pro implementaci vlastních serverů zpracovávajících dotazy a vytvářejících odpovědi (response). Ostatně právě existence tohoto balíčku měla poměrně velký vliv na oblíbenost programovacího jazyka Go pro tvorbu síťově orientovaných aplikací.

10. Poslání HTTP dotazu a základní zpracování odpovědi serveru

První příklad použití balíčku net/http, s nímž se dnes seznámíme, bude volat funkci pojmenovanou Get, která posílá na zvolený server (a jeho endpoint) dotaz HTTP metodou GET. Použití této funkce je velmi snadné – funkci se pouze předá celá URL a výsledkem je hodnota typu Response a příznak chyby:

response, err := http.Get("http://httpbin.org/uuid")

Hodnota typu Response obsahuje mj. i stavový kód (200, 301, 400, 404 atd. atd.), stavový kód reprezentovaný řetězcem („200 OK“) a taktéž tělo odpovědi (body). S obsahem těla odpovědi můžeme pracovat jako s datovým tokem, takže ho můžeme přečíst funkcí ioutil.ReadAll, převést výsledek na řetězec a ten vytisknout:

raw_body, err := ioutil.ReadAll(response.Body)
body := string(raw_body)
println(body)

Příklad použití funkce Get je ukázán v následujícím demonstračním příkladu:

package main
 
import (
        "fmt"
        "io/ioutil"
        "net/http"
)
 
func main() {
        response, err := http.Get("http://httpbin.org/uuid")
        if err != nil {
                println("Connection refused")
        }
        defer response.Body.Close()
 
        fmt.Printf("Status: %s\n", response.Status)
        fmt.Printf("Content length: %d\n", response.ContentLength)
 
        raw_body, err := ioutil.ReadAll(response.Body)
        if err != nil {
                println("Response body can't be read")
        } else {
                body := string(raw_body)
                println(body)
        }
}

Po spuštění tohoto příkladu by se na standardním výstupu měly objevit zhruba tyto řádky (samotné UUID bude ovšem takřka se stoprocentní jistotou odlišné):

Status: 200 OK
Content length: 53
{
  "uuid": "2dc037d0-fec2-4101-9b64-cb93e2e02bf9"
}
Poznámka: povšimněte si, že jsme získali tělo odpovědi, které evidentně obsahuje data ve formátu JSON. Práci s tímto formátem si popíšeme v navazující části tohoto seriálu.

11. Zobrazení hlaviček vrácených v odpovědi HTTP serveru

Předchozí příklad si můžeme nepatrně vylepšit tak, že vytiskneme i všechny hlavičky, které jsou poslány společně s odpovědí serveru:

for name, headers := range response.Header {
        name = strings.ToLower(name)
        for _, h := range headers {
                fmt.Printf("%v: %v\n", name, h)
        }
}

Následuje výpis zdrojového kódu tohoto příkladu:

package main
 
import (
        "fmt"
        "net/http"
        "strings"
)
 
func main() {
        response, err := http.Get("http://httpbin.org/uuid")
        if err != nil {
                println("Connection refused")
        }
        defer response.Body.Close()
 
        fmt.Printf("Status: %s\n", response.Status)
        fmt.Printf("Content length: %d\n", response.ContentLength)
 
        for name, headers := range response.Header {
                name = strings.ToLower(name)
                for _, h := range headers {
                        fmt.Printf("%v: %v\n", name, h)
                }
        }
}

Po překladu a spuštění příkladu získáme tento výstup obsahující jak stavový kód HTTP (200 OK), tak i délku těla odpovědi a všechny hlavičky:

Status: 200 OK
Content length: 53
access-control-allow-credentials: true
via: 1.1 vegur
connection: keep-alive
server: gunicorn/19.9.0
date: Sun, 10 Feb 2019 20:12:21 GMT
content-type: application/json
content-length: 53
access-control-allow-origin: *

12. Vytvoření jednoduchého HTTP serveru

Nejužitečnější vlastností balíčku net/http je jeho podpora pro vytvoření skutečného HTTP serveru, a to doslova na několika řádcích programového kódu. Základem je funkce HandleFunc, která nám umožňuje zaregistrovat obslužnou funkci (handler) v případě, že je server volán s určitým URL (endpointem). Můžeme si například zaregistrovat handler pro endpoint /:

http.HandleFunc("/", mainEndpoint)

Hlavička funkce HandleFunc vypadá takto:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

Povšimněte si, že druhým parametrem této funkce je jiná funkce (onen handler) s hlavičkou:

func MujHandler(ResponseWriter, *Request)

Konkrétně může implementace našeho handleru poslat na výstup (typu ResponseWriter) jednoduchý text, který bude zaslán klientovi v celé HTTP odpovědi (s hlavičkami, stavovým kódem, délkou atd. atd.):

func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "Hello world!\n")
}

Následně již stačí server spustit na určeném portu:

http.ListenAndServe(":8000", nil)

Úplná implementace jednoduchého HTTP serveru může vypadat takto:

package main
 
import (
        "io"
        "net/http"
)
 
func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "Hello world!\n")
}
 
func main() {
        http.HandleFunc("/", mainEndpoint)
        http.ListenAndServe(":8000", nil)
}

K otestování tohoto příkladu využijeme například utilitku curl:

$ curl localhost:8000
 
Hello world!

Popř. pro podrobnější výstup:

$ curl -v localhost:8000
 
* Rebuilt URL to: localhost:8000/
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8000
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 13 Feb 2019 19:31:17 GMT
< Content-Length: 13
< Content-Type: text/plain; charset=utf-8
<
Hello world!
* Connection #0 to host localhost left intact

13. Server s měnitelným stavem a problémy, které to způsobí

V další implementaci serveru jsou zaregistrovány dva handlery pro endpointy / a /counter:

http.HandleFunc("/", mainEndpoint)
http.HandleFunc("/counter", counterEndpoint)

Druhý handler vrátí klientovi obsah počitadla požadavků (počitadlo je postupně zvyšováno o jedničku):

var counter int
 
func counterEndpoint(writer http.ResponseWriter, request *http.Request) {
        counter++
        fmt.Fprintf(writer, "Counter: %d\n", counter)
}

Následuje výpis zdrojového kódu tohoto příkladu:

package main
 
import (
        "fmt"
        "io"
        "net/http"
)
 
var counter int
 
func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "Hello world!\n")
}
 
func counterEndpoint(writer http.ResponseWriter, request *http.Request) {
        counter++
        fmt.Fprintf(writer, "Counter: %d\n", counter)
}
 
func main() {
        http.HandleFunc("/", mainEndpoint)
        http.HandleFunc("/counter", counterEndpoint)
        http.ListenAndServe(":8000", nil)
}

Opět si ukažme příklad odpovědí serveru při posílání požadavků nástrojem curl:

$ curl localhost:8000
Hello world!
 
$ curl localhost:8000/counter
Counter: 1
 
$ curl localhost:8000/counter
Counter: 2
 
$ curl localhost:8000/counter
Counter: 3

14. Role mutexů v programovacím jazyce Go

Ve skutečnosti ovšem není předchozí příklad naprogramován korektně, protože jednotlivé dotazy klientů jsou automaticky zpracovávány v samostatných gorutinách (tudíž paralelně) a počitadlo je „pouze“ proměnná typu int. Paralelní přístup k této proměnné by tedy měl být synchronizován. K tomu využijeme takzvané mutexy z balíčku sync.

Vytvoření mutexu:

var mutex = &sync.Mutex{}

V handleru se přístup k proměnné counter, včetně jejího zvýšení o jedničku, provede uvnitř mutexu, který zajistí přístup pouze jediné gorutiny v daném okamžiku:

func counterEndpoint(writer http.ResponseWriter, request *http.Request) {
        mutex.Lock()
        counter++
        fmt.Fprintf(writer, "Counter: %d\n", counter)
        mutex.Unlock()
}
Poznámka: tento přístup by bylo v praxi nutné vylepšit, protože v sekci s mutexem se nachází i volání fmt.Fprintf, tedy potenciálně náročná operace. Můžeme použít lokální proměnnou s počitadlem, popř. namísto mutexů využít atomické typy definované v balíčku sync/atomic.

15. Úprava HTTP serveru takovým způsobem, aby používal mutexy

Korektně naprogramovaný server s počitadlem požadavků (tedy se stavem) bude vypadat takto:

package main
 
import (
        "fmt"
        "io"
        "net/http"
        "sync"
)
 
var counter int
var mutex = &sync.Mutex{}
 
func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "Hello world!\n")
}
 
func counterEndpoint(writer http.ResponseWriter, request *http.Request) {
        mutex.Lock()
        counter++
        fmt.Fprintf(writer, "Counter: %d\n", counter)
        mutex.Unlock()
}
 
func main() {
        http.HandleFunc("/", mainEndpoint)
        http.HandleFunc("/counter", counterEndpoint)
        http.ListenAndServe(":8000", nil)
}

Otestování základní funkcionality:

$ for i in {1..10};do curl localhost:8000/counter;done
 
Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5
Counter: 6
Counter: 7
Counter: 8
Counter: 9
Counter: 10

16. HTTP server nabízející statický obsah (soubory)

V případě, že potřebujeme vytvořit HTTP server, který bude klientům posílat statický obsah (nějaké soubory), můžeme implementaci napsat doslova na několik programových řádků, jak je to ostatně patrné i při pohledu na zdrojový kód dnešního předposledního demonstračního příkladu. Ve volané funkci http.FileServer(http.Dir()) lze specifikovat adresář se statickým obsahem:

package main
 
import (
        "net/http"
)
 
func main() {
        http.Handle("/", http.FileServer(http.Dir("")))
        http.ListenAndServe(":8000", nil)
}

Po spuštění tohoto serveru:

$ go run 15_file_server.go

Lze získat seznam souborů:

$ curl localhost:8000
 
<pre>
<a href="01B_simple_client_headers.go">01B_simple_client_headers.go</a>
<a href="01_simple_client.go">01_simple_client.go</a>
<a href="02B_simple_server_no_localhost.go">02B_simple_server_no_localhost.go</a>
<a href="02_simple_server.go">02_simple_server.go</a>
<a href="03_slow_server.go">03_slow_server.go</a>
<a href="04_multi_connection_server.go">04_multi_connection_server.go</a>
<a href="05_text_client.go">05_text_client.go</a>
<a href="06_text_server.go">06_text_server.go</a>
<a href="07_lookup.go">07_lookup.go</a>
<a href="08_parse_ip.go">08_parse_ip.go</a>
<a href="09_ipv4_constructor.go">09_ipv4_constructor.go</a>
<a href="10_http_get.go">10_http_get.go</a>
<a href="11_http_print_headers.go">11_http_print_headers.go</a>
<a href="12_http_server.go">12_http_server.go</a>
<a href="13_http_server_with_state.go">13_http_server_with_state.go</a>
<a href="14_http_server_with_state_mutex.go">14_http_server_with_state_mutex.go</a>
<a href="15_file_server.go">15_file_server.go</a>
<a href="16_custom_server.go">16_custom_server.go</a>
</pre>
V tomto případě bude typ obsahu nastaven na „text/html“.

Samozřejmě lze získat přímo obsah nějakého souboru, například:

$ curl localhost:8000/01_simple_client.go
 
import (
        "fmt"
        "net"
)
 
func main() {
        conn, err := net.Dial("tcp", "localhost:1234")
        if err != nil {
                println("Connection refused!")
        } else {
                var b [1]byte
                n, err := conn.Read(b[:])
                if err != nil {
                        println("No response!")
                } else {
                        if n == 1 {
                                fmt.Printf("Received %d byte: %v\n", n, b)
                        } else {
                                fmt.Printf("Received %d bytes: %v\n", n, b)
                        }
                }
        }
}
V tomto případě bude typ obsahu nastaven na „text/plain“.

17. Kombinovaný HTTP server poskytující statický i dynamický obsah

Dnešní poslední demonstrační příklad vznikl kombinací dvou HTTP serverů, které jsme si ukázali v předchozích kapitolách. Nový server nabízí jak statický obsah, tak i dva endpointy – jeden z nich vrací odpověď „Hello world!“, druhý pak textovou zprávu s celkovým počtem dotazů, které na tento endpoint přišly. Všechny tři endpointy jsou zaregistrovány takto:

http.HandleFunc("/", mainEndpoint)
http.HandleFunc("/counter", counterEndpoint)
http.HandleFunc("/files/", filesEndpoint)

Úpravou prošla zejména funkce, která vrací statický obsah, protože z URL odstraníme prefix „/files/“:

func filesEndpoint(writer http.ResponseWriter, request *http.Request) {
        url := request.URL.Path[len("/files/"):]
        println("Serving file from URL: " + url)
        http.ServeFile(writer, request, url)
}
Poznámka: zde je nutné upozornit na to, že manipulace s URL tímto způsobem nemusí být úplně bezpečná, protože se případný útočník může pokusit do URL vložit například absolutní cestu, relativní cestu s .. atd.

Úplný zdrojový kód dnešního posledního demonstračního příkladu naleznete na adrese https://github.com/tisnik/go-root/blob/master/article11/16_cus­tom_server.go:

package main
 
import (
        "fmt"
        "io"
        "net/http"
        "sync"
)
 
var counter int
var mutex = &sync.Mutex{}
 
func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "Hello world!\n")
}
 
func counterEndpoint(writer http.ResponseWriter, request *http.Request) {
        mutex.Lock()
        counter++
        fmt.Fprintf(writer, "Counter: %d\n", counter)
        mutex.Unlock()
}
 
func filesEndpoint(writer http.ResponseWriter, request *http.Request) {
        url := request.URL.Path[len("/files/"):]
        println("Serving file from URL: " + url)
        http.ServeFile(writer, request, url)
}
 
func main() {
        http.HandleFunc("/", mainEndpoint)
        http.HandleFunc("/counter", counterEndpoint)
        http.HandleFunc("/files/", filesEndpoint)
        http.ListenAndServe(":8000", nil)
}

18. Repositář s demonstračními příklady

Zdrojové kódy všech dnes popsaných demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má doslova několik kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

# Demonstrační příklad Popis Cesta
1 01_simple_client.go klient, který přečte ze serveru sekvenci bajtů https://github.com/tisnik/go-fedora/blob/master/article11/01_sim­ple_client.go
2 01B_simple_client_headers.go upravený klient, který vytiskne místní i vzdálenou adresu https://github.com/tisnik/go-fedora/blob/master/article11/01B_sim­ple_client_headers.go
3 02_simple_server.go jednoduchý server posílající jediný bajt klientovi https://github.com/tisnik/go-fedora/blob/master/article11/02_sim­ple_server.go
4 02B_simple_server_no_localhost.go úprava adresy v předchozím příkladu https://github.com/tisnik/go-fedora/blob/master/article11/02B_sim­ple_server_no_localhost.go
5 03_slow_server.go server odpovídající klientovi opožděně https://github.com/tisnik/go-fedora/blob/master/article11/03_slow_ser­ver.go
6 04_multi_connection_server.go server, který dokáže obsloužit více klientů současně https://github.com/tisnik/go-fedora/blob/master/article11/04_mul­ti_connection_server.go
       
7 05_text_client.go jednoduchý klient akceptující textový řádek https://github.com/tisnik/go-fedora/blob/master/article11/05_tex­t_client.go
8 06_text_server.go server posílající klientovi textová data https://github.com/tisnik/go-fedora/blob/master/article11/06_tex­t_server.go
9 06B_better_text_server.go vylepšený server posílající klientovi textová data https://github.com/tisnik/go-fedora/blob/master/article11/06B_bet­ter_text_server.go
10 06C_wrong_connection_close.go připojení je nutné ukončit v gorutině, ne mimo ni https://github.com/tisnik/go-fedora/blob/master/article11/06C_wron­g_connection_close.go
       
11 07_lookup.go překlad doménového jména na IP adresy https://github.com/tisnik/go-fedora/blob/master/article11/07_lo­okup.go
12 08_parse_ip.go parsing IP adresy https://github.com/tisnik/go-fedora/blob/master/article11/08_par­se_ip.go
13 09_ipv4_constructor.go konstruktor typu IPv4 https://github.com/tisnik/go-fedora/blob/master/article11/09_ip­v4_constructor.go
       
12 10_http_get.go použití HTTP metody GET https://github.com/tisnik/go-fedora/blob/master/article11/10_http_get­.go
13 11_http_print_headers.go vytištění hlavičky HTTP odpovědi https://github.com/tisnik/go-fedora/blob/master/article11/11_http_prin­t_headers.go
14 12_http_server.go nejjednodušší HTTP server s jediným endpointem https://github.com/tisnik/go-fedora/blob/master/article11/12_http_ser­ver.go
15 13_http_server_with_state.go HTTP server se stavovou proměnnou https://github.com/tisnik/go-fedora/blob/master/article11/13_http_ser­ver_with_state.go
16 14_http_server_with_state_mutex.go korektní práce se stavovou proměnnou https://github.com/tisnik/go-fedora/blob/master/article11/14_http_ser­ver_with_state_mutex.go
17 15_file_server.go HTTP server vracející statický obsah https://github.com/tisnik/go-fedora/blob/master/article11/15_fi­le_server.go
18 16_custom_server.go kombinace předchozích možností https://github.com/tisnik/go-fedora/blob/master/article11/16_cus­tom_server.go

20. Odkazy na Internetu

  1. Go Mutex Tutorial
    https://tutorialedge.net/golang/go-mutex-tutorial/
  2. Creating A Simple Web Server With Golang
    https://tutorialedge.net/go­lang/creating-simple-web-server-with-golang/
  3. Building a Web Server in Go
    https://thenewstack.io/building-a-web-server-in-go/
  4. How big is the pipe buffer?
    https://unix.stackexchange­.com/questions/11946/how-big-is-the-pipe-buffer
  5. How to turn off buffering of stdout in C
    https://stackoverflow.com/qu­estions/7876660/how-to-turn-off-buffering-of-stdout-in-c
  6. setbuf(3) – Linux man page
    https://linux.die.net/man/3/setbuf
  7. setvbuf(3) – Linux man page (stejný obsah jako předchozí stránka)
    https://linux.die.net/man/3/setvbuf
  8. Select waits on a group of channels
    https://yourbasic.org/golang/select-explained/
  9. Rob Pike: Simplicity is Complicated (video)
    http://www.golang.to/posts/dotgo-2015-rob-pike-simplicity-is-complicated-youtube-16893
  10. Algorithms to Go
    https://yourbasic.org/
  11. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů
    https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu/
  12. Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů: vlastní filtry a lexery
    https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu-vlastni-filtry-a-lexery/
  13. Go Defer Simplified with Practical Visuals
    https://blog.learngoprogram­ming.com/golang-defer-simplified-77d3b2b817ff
  14. 5 More Gotchas of Defer in Go — Part II
    https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-ii-cc550f6ad9aa
  15. The Go Blog: Defer, Panic, and Recover
    https://blog.golang.org/defer-panic-and-recover
  16. The defer keyword in Swift 2: try/finally done right
    https://www.hackingwithswift.com/new-syntax-swift-2-defer
  17. Swift Defer Statement
    https://andybargh.com/swift-defer-statement/
  18. Modulo operation (Wikipedia)
    https://en.wikipedia.org/wi­ki/Modulo_operation
  19. Node.js vs Golang: Battle of the Next-Gen Languages
    https://www.hostingadvice­.com/blog/nodejs-vs-golang/
  20. The Go Programming Language (home page)
    https://golang.org/
  21. GoDoc
    https://godoc.org/
  22. Go (programming language), Wikipedia
    https://en.wikipedia.org/wi­ki/Go_(programming_langua­ge)
  23. Go Books (kniha o jazyku Go)
    https://github.com/dariubs/GoBooks
  24. The Go Programming Language Specification
    https://golang.org/ref/spec
  25. Go: the Good, the Bad and the Ugly
    https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/
  26. Package builtin
    https://golang.org/pkg/builtin/
  27. Package fmt
    https://golang.org/pkg/fmt/
  28. The Little Go Book (další kniha)
    https://github.com/dariubs/GoBooks
  29. The Go Programming Language by Brian W. Kernighan, Alan A. A. Donovan
    https://www.safaribookson­line.com/library/view/the-go-programming/9780134190570/e­book_split010.html
  30. Learning Go
    https://www.miek.nl/go/
  31. Go Bootcamp
    http://www.golangbootcamp.com/
  32. Programming in Go: Creating Applications for the 21st Century (další kniha o jazyku Go)
    http://www.informit.com/sto­re/programming-in-go-creating-applications-for-the-21st-9780321774637
  33. Introducing Go (Build Reliable, Scalable Programs)
    http://shop.oreilly.com/pro­duct/0636920046516.do
  34. Learning Go Programming
    https://www.packtpub.com/application-development/learning-go-programming
  35. The Go Blog
    https://blog.golang.org/
  36. Getting to Go: The Journey of Go's Garbage Collector
    https://blog.golang.org/ismmkeynote
  37. Go (programovací jazyk, Wikipedia)
    https://cs.wikipedia.org/wi­ki/Go_(programovac%C3%AD_ja­zyk)
  38. Rychle, rychleji až úplně nejrychleji s jazykem Go
    https://www.root.cz/clanky/rychle-rychleji-az-uplne-nejrychleji-s-jazykem-go/
  39. Installing Go on the Raspberry Pi
    https://dave.cheney.net/2012/09/25/in­stalling-go-on-the-raspberry-pi
  40. How the Go runtime implements maps efficiently (without generics)
    https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics
  41. Niečo málo o Go – Golang (slovensky)
    http://golangsk.logdown.com/
  42. How Many Go Developers Are There?
    https://research.swtch.com/gop­hercount
  43. Most Popular Technologies (Stack Overflow Survery 2018)
    https://insights.stackover­flow.com/survey/2018/#most-popular-technologies
  44. Most Popular Technologies (Stack Overflow Survery 2017)
    https://insights.stackover­flow.com/survey/2017#techno­logy
  45. JavaScript vs. Golang for IoT: Is Gopher Winning?
    https://www.iotforall.com/javascript-vs-golang-iot/
  46. The Go Programming Language: Release History
    https://golang.org/doc/de­vel/release.html
  47. Go 1.11 Release Notes
    https://golang.org/doc/go1.11
  48. Go 1.10 Release Notes
    https://golang.org/doc/go1.10
  49. Go 1.9 Release Notes (tato verze je stále používána)
    https://golang.org/doc/go1.9
  50. Go 1.8 Release Notes (i tato verze je stále používána)
    https://golang.org/doc/go1.8
  51. Go on Fedora
    https://developer.fedorapro­ject.org/tech/languages/go/go-installation.html
  52. Writing Go programs
    https://developer.fedorapro­ject.org/tech/languages/go/go-programs.html
  53. The GOPATH environment variable
    https://tip.golang.org/doc/co­de.html#GOPATH
  54. Command gofmt
    https://tip.golang.org/cmd/gofmt/
  55. The Go Blog: go fmt your code
    https://blog.golang.org/go-fmt-your-code
  56. C? Go? Cgo!
    https://blog.golang.org/c-go-cgo
  57. Spaces vs. Tabs: A 20-Year Debate Reignited by Google’s Golang
    https://thenewstack.io/spaces-vs-tabs-a-20-year-debate-and-now-this-what-the-hell-is-wrong-with-go/
  58. 400,000 GitHub repositories, 1 billion files, 14 terabytes of code: Spaces or Tabs?
    https://medium.com/@hoffa/400–000-github-repositories-1-billion-files-14-terabytes-of-code-spaces-or-tabs-7cfe0b5dd7fd
  59. Gofmt No Longer Allows Spaces. Tabs Only
    https://news.ycombinator.com/i­tem?id=7914523
  60. Why does Go „go fmt“ uses tabs instead of whitespaces?
    https://www.quora.com/Why-does-Go-go-fmt-uses-tabs-instead-of-whitespaces
  61. Interactive: The Top Programming Languages 2018
    https://spectrum.ieee.org/sta­tic/interactive-the-top-programming-languages-2018
  62. Go vs. Python
    https://www.peterbe.com/plog/govspy
  63. PackageManagementTools
    https://github.com/golang/go/wi­ki/PackageManagementTools
  64. A Tour of Go: Type inference
    https://tour.golang.org/basics/14
  65. Go Slices: usage and internals
    https://blog.golang.org/go-slices-usage-and-internals
  66. Go by Example: Slices
    https://gobyexample.com/slices
  67. What is the point of slice type in Go?
    https://stackoverflow.com/qu­estions/2098874/what-is-the-point-of-slice-type-in-go
  68. The curious case of Golang array and slices
    https://medium.com/@hackintoshrao/the-curious-case-of-golang-array-and-slices-2565491d4335
  69. Introduction to Slices in Golang
    https://www.callicoder.com/golang-slices/
  70. Golang: Understanding ‚null‘ and nil
    https://newfivefour.com/golang-null-nil.html
  71. What does nil mean in golang?
    https://stackoverflow.com/qu­estions/35983118/what-does-nil-mean-in-golang
  72. nils In Go
    https://go101.org/article/nil.html
  73. Go slices are not dynamic arrays
    https://appliedgo.net/slices/
  74. Go-is-no-good (nelze brát doslova)
    https://github.com/ksimka/go-is-not-good
  75. Rust vs. Go
    https://news.ycombinator.com/i­tem?id=13430108
  76. Seriál Programovací jazyk Rust
    https://www.root.cz/seria­ly/programovaci-jazyk-rust/
  77. Modern garbage collection: A look at the Go GC strategy
    https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e
  78. Go GC: Prioritizing low latency and simplicity
    https://blog.golang.org/go15gc
  79. Is Golang a good language for embedded systems?
    https://www.quora.com/Is-Golang-a-good-language-for-embedded-systems
  80. Running GoLang on an STM32 MCU. A quick tutorial.
    https://www.mickmake.com/post/running-golang-on-an-mcu-a-quick-tutorial
  81. Go, Robot, Go! Golang Powered Robotics
    https://gobot.io/
  82. Emgo: Bare metal Go (language for programming embedded systems)
    https://github.com/ziutek/emgo
  83. UTF-8 history
    https://www.cl.cam.ac.uk/~mgk25/uc­s/utf-8-history.txt
  84. Less is exponentially more
    https://commandcenter.blog­spot.com/2012/06/less-is-exponentially-more.html
  85. Should I Rust, or Should I Go
    https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9
  86. Setting up and using gccgo
    https://golang.org/doc/install/gccgo
  87. Elastic Tabstops
    http://nickgravgaard.com/elastic-tabstops/
  88. Strings, bytes, runes and characters in Go
    https://blog.golang.org/strings
  89. Datový typ
    https://cs.wikipedia.org/wi­ki/Datov%C3%BD_typ
  90. Seriál o programovacím jazyku Rust: Základní (primitivní) datové typy
    https://www.root.cz/clanky/pro­gramovaci-jazyk-rust-nahrada-c-nebo-slepa-cesta/#k09
  91. Seriál o programovacím jazyku Rust: Vytvoření „řezu“ z pole
    https://www.root.cz/clanky/prace-s-poli-v-programovacim-jazyku-rust/#k06
  92. Seriál o programovacím jazyku Rust: Řezy (slice) vektoru
    https://www.root.cz/clanky/prace-s-vektory-v-programovacim-jazyku-rust/#k05
  93. Printf Format Strings
    https://www.cprogramming.com/tu­torial/printf-format-strings.html
  94. Java: String.format
    https://docs.oracle.com/ja­vase/8/docs/api/java/lang/Strin­g.html#format-java.lang.String-java.lang.Object…-
  95. Java: format string syntax
    https://docs.oracle.com/ja­vase/8/docs/api/java/util/For­matter.html#syntax
  96. Selectors
    https://golang.org/ref/spec#Selectors
  97. Calling Go code from Python code
    http://savorywatt.com/2015/09/18/ca­lling-go-code-from-python-code/
  98. Go Data Structures: Interfaces
    https://research.swtch.com/interfaces
  99. How to use interfaces in Go
    http://jordanorelli.com/pos­t/32665860244/how-to-use-interfaces-in-go
  100. Interfaces in Go (part I)
    https://medium.com/golangspec/in­terfaces-in-go-part-i-4ae53a97479c
  101. Part 21: Goroutines
    https://golangbot.com/goroutines/
  102. Part 22: Channels
    https://golangbot.com/channels/
  103. [Go] Lightweight eventbus with async compatibility for Go
    https://github.com/asaske­vich/EventBus
  104. What about Trait support in Golang?
    https://www.reddit.com/r/go­lang/comments/8mfykl/what_a­bout_trait_support_in_golan­g/
  105. Don't Get Bitten by Pointer vs Non-Pointer Method Receivers in Golang
    https://nathanleclaire.com/blog/2014/08/09/dont-get-bitten-by-pointer-vs-non-pointer-method-receivers-in-golang/
  106. Control Flow
    https://en.wikipedia.org/wi­ki/Control_flow
  107. Structured programming
    https://en.wikipedia.org/wi­ki/Structured_programming
  108. Control Structures
    https://www.golang-book.com/books/intro/5
  109. Control structures – Go if else statement
    http://golangtutorials.blog­spot.com/2011/06/control-structures-if-else-statement.html
  110. Control structures – Go switch case statement
    http://golangtutorials.blog­spot.com/2011/06/control-structures-go-switch-case.html
  111. Control structures – Go for loop, break, continue, range
    http://golangtutorials.blog­spot.com/2011/06/control-structures-go-for-loop-break.html
  112. Single Function Exit Point
    http://wiki.c2.com/?Single­FunctionExitPoint
  113. Entry point
    https://en.wikipedia.org/wi­ki/Entry_point
  114. Why does Go have a GOTO statement?!
    https://www.reddit.com/r/go­lang/comments/kag5q/why_do­es_go_have_a_goto_statemen­t/
  115. Effective Go
    https://golang.org/doc/ef­fective_go.html
  116. GoClipse: an Eclipse IDE for the Go programming language
    http://goclipse.github.io/
  117. GoClipse Installation
    https://github.com/GoClip­se/goclipse/blob/latest/do­cumentation/Installation.md#in­stallation
Našli jste v článku chybu?