Užitečná novinka v Go 1.22: vylepšení směrování v knihovně net/http

31. 10. 2024
Doba čtení: 30 minut

Sdílet

 Autor: Go lang
Součástí standardní knihovny jazyka Go je mj. i balíček net/http, který umožňuje tvorbu HTTP klientů i serverů. V Go verze 1.22 došlo k vylepšení možností tohoto balíčku, které umožňují tvořit webové služby s REST API.

Obsah

1. Užitečná novinka v Go 1.22: vylepšení směrování v knihovně net/http

2. Jednoduchý HTTP server využívající rozhraní ResponseWriter a strukturu Request

3. HTTP server využívající knihovnu gorilla/mux

4. Specifikace HTTP metody pro jednotlivé handlery

5. Kostra webové služby pro správu osob

6. Nové možnosti při směrování dotazů v Go 1.22

7. Webová služba umožňující manipulace se seznamem uživatelů

8. Realizace handleru pro výpis seznamu uživatelů

9. Úplný zdrojový kód první verze webové služby

10. Otestování webové služby

11. Úplný zdrojový kód druhé verze webové služby

12. Otestování chování upravené varianty webové služby

13. Kontrola, zda je při volání endpointu použita očekávaná metoda

14. Úplný zdrojový kód třetí verze webové služby

15. Otestování, jak webová služba kontroluje HTTP metody požadavků

16. Nedostatky webové služby a jejich náprava v Go 1.22

17. Úplný zdrojový kód čtvrté verze webové služby

18. Otestování základních funkcí poslední varianty webové služby

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

20. Odkazy na Internetu

1. Užitečná novinka v Go 1.22: vylepšení směrování v knihovně net/http

Programovací jazyk Go je mezi vývojáři oblíbený mj. i z toho důvodu, že jeho standardní knihovny obsahují množství rozhraní, datových struktur a funkcí, které lze přímo využít při tvorbě aplikací pracujících s různými síťovými protokoly. Nalezneme zde například knihovnu net/http určenou pro komunikaci s využitím známého HTTP protokolu. Standardní knihovna net/http umožňuje tvořit jak HTTP klienty, tak i servery.

Ovšem právě v oblasti HTTP serverů standardní knihovně jazyka Go „něco“ chybělo k tomu, aby ji bylo možné samostatně, plnohodnotně a bezproblémově využít. Jednalo se o nedostatečné možnosti při směrování požadavků (tedy pro mapování mezi adresami koncových bodů a handlery, které zpracovávají příslušné požadavky) a taktéž o problematické určení, které HTTP metody je možné pro dané koncové body využít. To se změnilo v Go verze 1.22, kde došlo ke dvěma vylepšením právě v těchto oblastech. Od Go 1.22 se tedy můžeme (alespoň v některých situacích) obejít bez knihoven třetích stran, například bez knihovny Gorilla/mux atd.

2. Jednoduchý HTTP server využívající rozhraní ResponseWriter a strukturu Request

Nejprve si připomeňme, jakým způsobem je vlastně možné s využitím výše uvedeného standardního balíčku net/http vytvořit jednoduchý HTTP server. Pravděpodobně nejjednodušší podoba takového serveru má zaregistrován pouze jediný handler, tedy funkci, která je volána při zadání adresy localhost:8080 nebo localhost:8080/ (nebo v tomto případě prakticky jakéhokoli jiného koncového bodu). Odpověď serveru bude vždy stejná – HTTP 200 OK a zpráva bude znít „Hello world!“:

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)
}

Zajímavější a vlastně i mnohem užitečnější je ovšem pochopitelně implementace HTTP serveru, která bude generovat dynamický obsah. Ten lze tvořit buď přímo „ručně“ v programu, nebo můžeme využít některé balíčky ze standardní knihovny programovacího jazyka Go pro generování dat ve formátu JSON, XML, popř. knihovny s implementací šablon (templates). Dnes nás ovšem bude primárně zajímat první způsob, tj. „ruční“ generování odpovědi, která je serverem posílána klientovi na základě jeho dotazu (request). Jedna z nejjednodušších implementací takového HTTP serveru může vypadat například následovně:

package main
 
import (
        "net/http"
)
 
func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        response := "Hello world!\n"
        writer.Write([]byte(response))
}
 
func main() {
        http.HandleFunc("/", mainEndpoint)
        http.ListenAndServe(":8000", nil)
}

Funkci takového serveru si můžeme snadno otestovat, například s využitím nástroje wget nebo ještě lépe curl. Vzhledem k tomu, že víme, na jakém portu server běží a jaký endpoint máme zavolat, sestavíme příkaz pro curl tímto způsobem:

$ curl -v localhost:8000

Výstup bude obsahovat i ladicí informace vyžádané přepínačem -v:

* 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: Mon, 06 May 2019 18:14:06 GMT
< Content-Length: 13
< Content-Type: text/plain; charset=utf-8
<
Hello world!
* Connection #0 to host localhost left intact

Ve zdrojovém kódu si povšimněte především funkce, v níž je implementováno generování a posílání odpovědi. Této funkci jsou předány dvě hodnoty, přičemž první je typu rozhraní http.ResponseWriter a druhá je typu ukazatel na http.Request:

type Request struct {
        Method string
        URL *url.URL
        Proto      string // "HTTP/1.0"
        ProtoMajor int    // 1
        ProtoMinor int    // 0
        Header Header
        Body io.ReadCloser
        GetBody func() (io.ReadCloser, error)
        ContentLength int64
        TransferEncoding []string
        Close bool
        Host string
        Form url.Values
        PostForm url.Values
        MultipartForm *multipart.Form
        Trailer Header
        RemoteAddr string
        RequestURI string
        TLS *tls.ConnectionState
        Cancel <-chan struct{}
        Response *Response
        Pattern string
}

a:

type ResponseWriter interface {
        Header() Header
        Write([]byte) (int, error)
        WriteHeader(statusCode int)
}

3. HTTP server využívající knihovnu gorilla/mux

Nyní se podívejme na způsob realizace jednoduchého HTTP serveru, který bude mj. používat i (nestandardní) balíček gorilla/mux pocházející z Gorilla Toolkitu. Základní služby poskytované serverem budou stejné, ovšem navíc budeme implementovat koncový bod, který vrátí obsah čítače (interně se zvyšuje při každém přístupu). Požadované změny nastanou náhradou následujících dvou řádků s registrací handlerů, které bychom použili v net/http:

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

V upraveném zdrojovém kódu demonstračního příkladu použijeme takzvaný směrovač neboli router poskytovaný právě knihovnou gorilla/mux. Jeho konstrukce může vypadat následovně:

router := mux.NewRouter()

Popř. můžeme explicitně specifikovat, zda se budou URI typu /cesta a /cesta/ považovat za shodné či nikoli:

router := mux.NewRouter().StrictSlash(true)

Dále zaregistrujeme oba výše zmíněné handlery, ovšem nyní použijeme metodu router.HandleFunc a nikoli funkci http.HandleFunc (z balíčku net/http):

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

Nakonec je pochopitelně nutné náš nově upravený HTTP server spustit. Povšimněte si, že se v tomto případě využije druhý parametr funkce http.ListenAndServe – již se zde nepředává hodnota nil, ale instance právě nakonfigurovaného směrovače:

err := http.ListenAndServe(ADDRESS, router)

Úplný zdrojový kód upraveného příkladu, který naleznete na adrese https://github.com/tisnik/go-root/blob/master/article38/02_http_ser­ver_with_mux.go, vypadá následovně:

package main
 
import (
        "fmt"
        "github.com/gorilla/mux"
        "io"
        "log"
        "net/http"
        "os"
        "sync"
)
 
const ADDRESS = ":8080"
 
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() {
        router := mux.NewRouter().StrictSlash(true)
 
        router.HandleFunc("/", mainEndpoint)
        router.HandleFunc("/counter", counterEndpoint)
 
        log.Println("Starting HTTP server at address", ADDRESS)
        err := http.ListenAndServe(ADDRESS, router)
        if err != nil {
                log.Fatal("Unable to initialize HTTP server", err)
                os.Exit(2)
        }
}

Funkcionalitu tohoto příkladu snadno otestujeme, a to opět s využitím nástroje curl:

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

Otestování funkce čítače:

$ curl -v localhost:8080/counter
 
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /counter HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 13 Oct 2019 16:25:48 GMT
< Content-Length: 11
< Content-Type: text/plain; charset=utf-8
<
Counter: 1
* Connection #0 to host localhost left intact

4. Specifikace HTTP metody pro jednotlivé handlery

V případě, že při volání demonstračního příkladu z předchozí kapitoly použijeme jinou HTTP metodu než GET (což je pro nástroj curl výchozí metoda, pokud ovšem nebudeme na server posílat data), bude například čítač stále přístupný. O tom se ostatně můžeme velmi snadno přesvědčit, pokud budeme explicitně specifikovat metodu POST, PUT či dokonce DELETE:

$ curl -v -X POST localhost:8080/counter
 
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /counter HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 13 Oct 2019 16:31:50 GMT
< Content-Length: 11
< Content-Type: text/plain; charset=utf-8
<
Counter: 2
* Connection #0 to host localhost left intact
 
$ curl -v -X DELETE localhost:8080/counter
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> DELETE /counter HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 13 Oct 2019 16:31:56 GMT
< Content-Length: 11
< Content-Type: text/plain; charset=utf-8
<
Counter: 3
* Connection #0 to host localhost left intact

Takové chování ovšem většinou u služeb postavených nad REST API není ideální, protože s prostředky, které jsou přes API obsluhovány, se provádí různé operace typu CRUD. Samozřejmě je možné i při použití základního balíčku net/http získat jméno použité metody, ovšem nejedná se o ideální řešení. To nám nabízí již zmíněný balíček gorilla/mux, v němž můžeme omezit volání handleru pouze pro danou metodu. V našem demonstračním příkladu prozatím pouze čteme hodnoty (prostředků) a neměníme je, takže nám postačuje použít metodu GET omezit použití ostatních metod:

router := mux.NewRouter().StrictSlash(true)
 
router.HandleFunc("/", mainEndpoint).Methods("GET")
router.HandleFunc("/counter", counterEndpoint).Methods("GET")

Upravený zdrojový kód demonstračního příkladu bude vypadat následovně:

package main
 
import (
        "fmt"
        "github.com/gorilla/mux"
        "io"
        "log"
        "net/http"
        "os"
        "sync"
)
 
const ADDRESS = ":8080"
 
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() {
        router := mux.NewRouter().StrictSlash(true)
 
        router.HandleFunc("/", mainEndpoint).Methods("GET")
        router.HandleFunc("/counter", counterEndpoint).Methods("GET")
 
        log.Println("Starting HTTP server at address", ADDRESS)
        err := http.ListenAndServe(ADDRESS, router)
        if err != nil {
                log.Fatal("Unable to initialize HTTP server", err)
                os.Exit(2)
        }
}

Můžeme si ihned otestovat, jak se bude nová služba chovat při použití různých HTTP metod.

Výchozí metoda GET:

$ curl -v localhost:8080/
 
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 13 Oct 2019 18:45:33 GMT
< Content-Length: 13
< Content-Type: text/plain; charset=utf-8
<
Hello world!

Metoda PUT:

$ curl -v -X PUT localhost:8080/
 
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> PUT / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 405 Method Not Allowed
< Date: Sun, 13 Oct 2019 18:45:37 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Metoda POST:

$ curl -v -X POST localhost:8080/
 
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 405 Method Not Allowed
< Date: Sun, 13 Oct 2019 18:45:42 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Metoda DELETE:

$ curl -v -X DELETE localhost:8080/
 
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> DELETE / HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 405 Method Not Allowed
< Date: Sun, 13 Oct 2019 18:45:45 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Poznámka: povšimněte si, že se hodnota čítače vrátila pouze při použití metody GET. Snaha o použití jiných metod vede k chybovému stavu HTTP/1.1 405 Method Not Allowed, viz též 4×x Client errors (chyba na straně klienta – poslal špatný dotaz).

5. Kostra webové služby pro správu osob

Ukažme si ještě, jak by mohla vypadat kostra webové služby pro správu osob. K dispozici budou tyto operace:

# Operace Volání Metoda
1 výpis celé databáze /users GET
2 informace o zvolené osobě /user/ID_OSOBY GET
3 přidání nové osoby do databáze /user/ID_OSOBY POST
4 změna údajů v databázi /user/ID_OSOBY PUT
5 vymazání osoby /user/ID_OSOBY DELETE

Nejprve použijeme Gorilla/mux. Při specifikaci handlerů využijeme toho, že (proměnné) jméno prostředku lze uzavřít do složených závorek:

router.HandleFunc("/user", listAllUsersEndpoint).Methods("GET")
router.HandleFunc("/user/{id}", getUserEndpoint).Methods("GET")
router.HandleFunc("/user/{id}", createUserEndpoint).Methods("POST")
router.HandleFunc("/user/{id}", updateUserEndpoint).Methods("PUT")
router.HandleFunc("/user/{id}", deleteUserEndpoint).Methods("DELETE")

Kostra této služby, pro stručnost bez implementace jednotlivých operací v handlerech, může vypadat následovně:

package main
 
import (
        "github.com/gorilla/mux"
        "io"
        "log"
        "net/http"
        "os"
)
 
const ADDRESS = ":8080"
 
func mainEndpoint(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "Hello world!\n")
}
 
func listAllUsersEndpoint(writer http.ResponseWriter, request *http.Request) {
        io.WriteString(writer, "LIST ALL PERSONS\n")
}
 
func getUserEndpoint(writer http.ResponseWriter, request *http.Request) {
        id := mux.Vars(request)["id"]
        person, found := persons[id]
        ...
        ...
        ...
 
        io.WriteString(writer, "GET PERSON\n")
}
 
func createUserEndpoint(writer http.ResponseWriter, request *http.Request) {
        id := mux.Vars(request)["id"]
        person, found := persons[id]
        ...
        ...
        ...
 
        io.WriteString(writer, "CREATE PERSON\n")
}
 
func updateUserEndpoint(writer http.ResponseWriter, request *http.Request) {
        id := mux.Vars(request)["id"]
        person, found := persons[id]
        ...
        ...
        ...
 
        io.WriteString(writer, "UPDATE PERSON\n")
}
 
func deleteUserEndpoint(writer http.ResponseWriter, request *http.Request) {
        id := mux.Vars(request)["id"]
        person, found := persons[id]
        ...
        ...
        ...
 
        io.WriteString(writer, "DELETE PERSON\n")
}
 
func main() {
        router := mux.NewRouter().StrictSlash(true)
 
        router.HandleFunc("/", mainEndpoint).Methods("GET")
        router.HandleFunc("/person", listAllUsersEndpoint).Methods("GET")
        router.HandleFunc("/person/{id}", getUserEndpoint).Methods("GET")
        router.HandleFunc("/person/{id}", createUserEndpoint).Methods("POST")
        router.HandleFunc("/person/{id}", updateUserEndpoint).Methods("PUT")
        router.HandleFunc("/person/{id}", deleteUserEndpoint).Methods("DELETE")
 
        log.Println("Starting HTTP server at address", ADDRESS)
        err := http.ListenAndServe(ADDRESS, router)
        if err != nil {
                log.Fatal("Unable to initialize HTTP server", err)
                os.Exit(2)
        }
}
Poznámka: povšimněte si způsobu přečtení ID, který je součástí adresy.

6. Nové možnosti při směrování dotazů v Go 1.22

V základním balíčku programovacího jazyka Go, konkrétně v balíčku net/http, se rozšířily možnosti specifikace vazby mezi adresou endpointu a příslušným handlerem. Nově je možné přímo při registraci handleru definovat, která HTTP metoda se bude používat. A navíc je možné, aby se při specifikaci endpointu určily i proměnné části, což jsou typicky identifikátory zdrojů (resources), například ID uživatelů atd. Tyto proměnné části jsou nejenom korektně rozpoznány, ale navíc je možné snadno získat i jejich konkrétní hodnoty, tedy například o jaké konkrétní ID uživatele se jedná.

To ovšem znamená, že vlastně byla implementována nejdůležitější funkcionalita z Gorilla/mux. To poměrně dobře odpovídá filozofii jazyka Go, který se snaží minimálně v oblasti síťových aplikací nabízet již ve standardní knihovně většinu potřebné funkcionality, čímž se omezí nutnost instalace a importu dalších knihoven (čemuž se ovšem obecně stejně nedokážeme vyhnout, což má většinou i nepříjemné důsledky v praxi).

Poznámka: na tomto místě je nutné poznamenat, že se konkrétní implementace mezi net/http a gorilla/mux liší.

7. Webová služba umožňující manipulace se seznamem uživatelů

V rámci navazujících kapitol si ukážeme realizaci webové služby určené pro správu osob, resp. přesněji řečeno její značně zjednodušenou variantu, kterou je ovšem možné snadno rozšířit o chybějící funkce. Informace o jedné osobě je tvořena touto datovou strukturou:

// Datova struktura predstavujici uzivatele
type User struct {
        Name    string
        Surname string
}

Pro práci s databází osob nám bude stačit několik metod, které jsou předepsány v rozhraní UserStorage:

// Rozhrani s predpisem metod pro manipulace s databazi uzivatelu
type UserStorage interface {
        ReadListOfUsers() []User
        ReadUser(ID int) (User, bool)
        DeleteUser(ID int)
}

Vlastní implementace databáze osob bude velmi jednoduchá, protože namísto reálné relační či objektové databáze použijeme seznam osob uložených jen v operační paměti. Každá osoba bude uložena do mapy obsahující následující datové struktury (klíčem bude ID):

// Implementace databaze uzivatelu v operacni pameti
type MemoryStorage struct {
        users map[int]User
}
 
// Inicializace databaze uzivatelu
func NewMemoryStorage() MemoryStorage {
        users := make(map[int]User)
        users[1] = User{
                Name:    "Linus",
                Surname: "Torvalds",
        }
        users[2] = User{
                Name:    "Rob",
                Surname: "Pike",
        }
        users[3] = User{
                Name:    "Ken",
                Surname: "Iverson",
        }

        return MemoryStorage{
                users: users,
        }
}
 
// Implementace vsech metod predepsanych rozhranim UserStorage
 
func (s MemoryStorage) ReadListOfUsers() []User {
        users := make([]User, 0, len(s.users))
        for _, user := range s.users {
                users = append(users, user)
        }
        return users
}
 
func (s MemoryStorage) ReadUser(ID int) (User, bool) {
        user, found := s.users[ID]
        return user, found
}
 
func (s MemoryStorage) DeleteUser(ID int) {
        delete(s.users, ID)
}
Poznámka: přidání další realizace databáze (například reálné relační databáze) je již snadné – pouze je nutné splnit (satisfy) rozhraní UserStorage.

8. Realizace handleru pro výpis seznamu uživatelů

Podívejme se nyní na to, jak by mohla vypadat realizace handleru, který bude zavolán při přístupu na koncový bod /users. V takovém případě by se měl vrátit JSON se všemi uživateli v databázi. Registrace příslušného handleru je následující:

        // REST API endpoints
        http.HandleFunc("/users", s.returnListOfUsers)

Samotný handler nejprve přečte informace o všech uživatelích uložených v databázi a následně seznam serializuje do JSONu, který je poslán zpět klientovi. Vše lze tedy realizovat jen na několika řádcích zdrojového kódu (ovšem bez kontroly chyb!):

// REST API handlery
 
func (s ServerImpl) returnListOfUsers(writer http.ResponseWriter, r *http.Request) {
        users := s.storage.ReadListOfUsers()
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(users)
}

9. Úplný zdrojový kód první verze webové služby

Následuje výpis celého zdrojového kódu první verze naší webové služby:

package main
 
import (
        "encoding/json"
        "fmt"
        "log"
        "net/http"
)
 
// Datova struktura predstavujici uzivatele
type User struct {
        Name    string
        Surname string
}
 
// Rozhrani s predpisem metod pro manipulace s databazi uzivatelu
type UserStorage interface {
        ReadListOfUsers() []User
        ReadUser(ID int) (User, bool)
        DeleteUser(ID int)
}
 
// Implementace databaze uzivatelu v operacni pameti
type MemoryStorage struct {
        users map[int]User
}
 
// Inicializace databaze uzivatelu
func NewMemoryStorage() MemoryStorage {
        users := make(map[int]User)
        users[1] = User{
                Name:    "Linus",
                Surname: "Torvalds",
        }
        users[2] = User{
                Name:    "Rob",
                Surname: "Pike",
        }
        users[3] = User{
                Name:    "Ken",
                Surname: "Iverson",
        }

        return MemoryStorage{
                users: users,
        }
}
 
// Implementace vsech metod predepsanych rozhranim UserStorage
 
func (s MemoryStorage) ReadListOfUsers() []User {
        users := make([]User, 0, len(s.users))
        for _, user := range s.users {
                users = append(users, user)
        }
        return users
}
 
func (s MemoryStorage) ReadUser(ID int) (User, bool) {
        user, found := s.users[ID]
        return user, found
}
 
func (s MemoryStorage) DeleteUser(ID int) {
        delete(s.users, ID)
}
 
// Rozhrani predepisujici metody serveru
type Server interface {
        Serve(port uint)
}
 
// Implementace HTTPServeru
type ServerImpl struct {
        storage UserStorage
}
 
// Inicialiace serveru, predani kontextu
func NewServer(storage UserStorage) Server {
        return ServerImpl{
                storage: storage,
        }
}
 
func (s ServerImpl) Serve(port uint) {
        log.Printf("Starting server on port 8080")
 
        // REST API endpoints
        http.HandleFunc("/users", s.returnListOfUsers)
 
        // start the server
        http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}
 
// REST API handlery
 
func (s ServerImpl) returnListOfUsers(writer http.ResponseWriter, r *http.Request) {
        users := s.storage.ReadListOfUsers()
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(users)
}
 
func main() {
        storage := NewMemoryStorage()
        server := NewServer(storage)
        server.Serve(8080)
}

10. Otestování webové služby

Otestování této primitivní webové služby je snadné – postačuje nám použít nástroj curl:

$ curl localhost:8080/users | jq .

S výsledkem:

[
  {
    "Name": "Linus",
    "Surname": "Torvalds"
  },
  {
    "Name": "Rob",
    "Surname": "Pike"
  },
  {
    "Name": "Ken",
    "Surname": "Iverson"
  }
]

11. Úplný zdrojový kód druhé verze webové služby

Druhá varianta webové služby obsahující handlery určené pro výpis konkrétního uživatele, popř. pro smazání uživatele identifikovaného svým ID, vypadá následovně. Povšimněte si určitých problémů, například opakujícího se kódu, použití „sloves“ v koncových bodech atd.:

package main
 
import (
        "encoding/json"
        "fmt"
        "log"
        "net/http"
        "strconv"
)
 
// Datova struktura predstavujici uzivatele
type User struct {
        Name    string
        Surname string
}
 
// Rozhrani s predpisem metod pro manipulace s databazi uzivatelu
type UserStorage interface {
        ReadListOfUsers() []User
        ReadUser(ID int) (User, bool)
        DeleteUser(ID int)
}
 
// Implementace databaze uzivatelu v operacni pameti
type MemoryStorage struct {
        users map[int]User
}
 
// Inicializace databaze uzivatelu
func NewMemoryStorage() MemoryStorage {
        users := make(map[int]User)
        users[1] = User{
                Name:    "Linus",
                Surname: "Torvalds",
        }
        users[2] = User{
                Name:    "Rob",
                Surname: "Pike",
        }
        users[3] = User{
                Name:    "Ken",
                Surname: "Iverson",
        }

        return MemoryStorage{
                users: users,
        }
}
 
// Implementace vsech metod predepsanych rozhranim UserStorage
 
func (s MemoryStorage) ReadListOfUsers() []User {
        users := make([]User, 0, len(s.users))
        for _, user := range s.users {
                users = append(users, user)
        }
        return users
}
 
func (s MemoryStorage) ReadUser(ID int) (User, bool) {
        user, found := s.users[ID]
        return user, found
}
 
func (s MemoryStorage) DeleteUser(ID int) {
        delete(s.users, ID)
}
 
// Rozhrani predepisujici metody serveru
type Server interface {
        Serve(port uint)
}
 
// Implementace HTTPServeru
type ServerImpl struct {
        storage UserStorage
}
 
// Inicialiace serveru, predani kontextu
func NewServer(storage UserStorage) Server {
        return ServerImpl{
                storage: storage,
        }
}
 
func (s ServerImpl) Serve(port uint) {
        log.Printf("Starting server on port 8080")
 
        // REST API endpoints
        http.HandleFunc("/users", s.returnListOfUsers)
        http.HandleFunc("/user", s.returnOneUser)
        http.HandleFunc("/delete-user", s.deleteOneUser)
 
        // start the server
        http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}
 
// REST API handlery
 
func (s ServerImpl) returnListOfUsers(writer http.ResponseWriter, r *http.Request) {
        users := s.storage.ReadListOfUsers()
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(users)
}
 
func (s ServerImpl) returnOneUser(writer http.ResponseWriter, r *http.Request) {
        params := r.URL.Query()
        IDs, found := params["ID"]
        if !found {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        ID, err := strconv.Atoi(IDs[0])
        if err != nil {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        user, found := s.storage.ReadUser(ID)
        if !found {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusNotFound)
                return
        }
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(user)
}
 
func (s ServerImpl) deleteOneUser(writer http.ResponseWriter, r *http.Request) {
        params := r.URL.Query()
        IDs, found := params["ID"]
        if !found {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        ID, err := strconv.Atoi(IDs[0])
        if err != nil {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        s.storage.DeleteUser(ID)
 
        writer.Header().Set("Content-Type", "application/json")
        status := struct {
                Status string
                ID     int
        }{"deleted", ID}
        json.NewEncoder(writer).Encode(status)
}
 
func main() {
        storage := NewMemoryStorage()
        server := NewServer(storage)
        server.Serve(8080)
}

12. Otestování chování upravené varianty webové služby

Po spuštění webové služby si otestujeme chování všech tří koncových bodů, které jsme implementovali:

$ curl localhost:8080/users | jq .
 
[
  {
    "Name": "Linus",
    "Surname": "Torvalds"
  },
  {
    "Name": "Rob",
    "Surname": "Pike"
  },
  {
    "Name": "Ken",
    "Surname": "Iverson"
  }
]
 
 
 
$ curl localhost:8080/user?ID=1 | jq .
 
{
  "Name": "Linus",
  "Surname": "Torvalds"
}
 
 
 
$ curl localhost:8080/delete-user?ID=1
 
{"Status":"deleted","ID":1}

Otestovat si můžeme i chování při chybných vstupech:

$ curl -v localhost:8080/delete-user
 
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /delete-user HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.0.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Content-Type: text/plain
< Date: Wed, 30 Oct 2024 10:13:17 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Navíc můžeme použít i HTTP metody, které nejsou v daném kontextu očekávány (což je implementační nedostatek):

$ curl -X DELETE localhost:8080/users
 
[{"Name":"Rob","Surname":"Pike"},{"Name":"Ken","Surname":"Iverson"}]

13. Kontrola, zda je při volání endpointu použita očekávaná metoda

Abychom zabránili tomu, že bude nějaký endpoint volán nekorektní HTTP metodou, můžeme i ve starších verzích ekosystému jazyka Go explicitně zkontrolovat, jaká metoda byla použita. K tomu slouží atribut request.Method, který přímo obsahuje konstantu http.MethodGet, http.MethodPut atd. Kontrolu je tedy možné provést například následujícím způsobem:

func (s ServerImpl) returnListOfUsers(writer http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
        ...
        ...
        ...
}

Popř. test, zda je pro smazání uživatele skutečně použita HTTP metoda DELETE:

func (s ServerImpl) deleteOneUser(writer http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodDelete {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
        ...
        ...
        ...
}

Tyto kontroly sice nejsou ideální, ale v předchozích verzích Go nám nic jiného nezbývalo, pokud jsme nechtěli využít gorilla/mux a podobné knihovny třetích stran.

14. Úplný zdrojový kód třetí verze webové služby

Po výše zmíněných úpravách bude naše webová služba vypadat následovně:

package main
 
import (
        "encoding/json"
        "fmt"
        "log"
        "net/http"
        "strconv"
)
 
// Datova struktura predstavujici uzivatele
type User struct {
        Name    string
        Surname string
}
 
// Rozhrani s predpisem metod pro manipulace s databazi uzivatelu
type UserStorage interface {
        ReadListOfUsers() []User
        ReadUser(ID int) (User, bool)
        DeleteUser(ID int)
}
 
// Implementace databaze uzivatelu v operacni pameti
type MemoryStorage struct {
        users map[int]User
}
 
// Inicializace databaze uzivatelu
func NewMemoryStorage() MemoryStorage {
        users := make(map[int]User)
        users[1] = User{
                Name:    "Linus",
                Surname: "Torvalds",
        }
        users[2] = User{
                Name:    "Rob",
                Surname: "Pike",
        }
        users[3] = User{
                Name:    "Ken",
                Surname: "Iverson",
        }

        return MemoryStorage{
                users: users,
        }
}
 
// Implementace vsech metod predepsanych rozhranim UserStorage
 
func (s MemoryStorage) ReadListOfUsers() []User {
        users := make([]User, 0, len(s.users))
        for _, user := range s.users {
                users = append(users, user)
        }
        return users
}
 
func (s MemoryStorage) ReadUser(ID int) (User, bool) {
        user, found := s.users[ID]
        return user, found
}
 
func (s MemoryStorage) DeleteUser(ID int) {
        delete(s.users, ID)
}
 
// Rozhrani predepisujici metody serveru
type Server interface {
        Serve(port uint)
}
 
// Implementace HTTPServeru
type ServerImpl struct {
        storage UserStorage
}
 
// Inicialiace serveru, predani kontextu
func NewServer(storage UserStorage) Server {
        return ServerImpl{
                storage: storage,
        }
}
 
func (s ServerImpl) Serve(port uint) {
        log.Printf("Starting server on port 8080")
 
        // REST API endpoints
        http.HandleFunc("/users", s.returnListOfUsers)
        http.HandleFunc("/user", s.returnOneUser)
        http.HandleFunc("/delete-user", s.deleteOneUser)
 
        // start the server
        http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}
 
// REST API handlery
 
func (s ServerImpl) returnListOfUsers(writer http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
        users := s.storage.ReadListOfUsers()
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(users)
}
 
func (s ServerImpl) returnOneUser(writer http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
        params := r.URL.Query()
        IDs, found := params["ID"]
        if !found {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        ID, err := strconv.Atoi(IDs[0])
        if err != nil {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        user, found := s.storage.ReadUser(ID)
        if !found {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusNotFound)
                return
        }
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(user)
}
 
func (s ServerImpl) deleteOneUser(writer http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodDelete {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
        params := r.URL.Query()
        IDs, found := params["ID"]
        if !found {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        ID, err := strconv.Atoi(IDs[0])
        if err != nil {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        s.storage.DeleteUser(ID)
 
        writer.Header().Set("Content-Type", "application/json")
        status := struct {
                Status string
                ID     int
        }{"deleted", ID}
        json.NewEncoder(writer).Encode(status)
}
 
func main() {
        storage := NewMemoryStorage()
        server := NewServer(storage)
        server.Serve(8080)
}

15. Otestování, jak webová služba kontroluje HTTP metody požadavků

Pokusme se nyní zavolat koncové body upravené webové služby tak, že vždy použijeme HTTP metody, které neodpovídají daným koncovým bodům:

$ curl -v -X DELETE localhost:8080/users
 
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> DELETE /users HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.0.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Content-Type: text/plain
< Date: Wed, 30 Oct 2024 12:49:30 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
$ curl -v -X GET localhost:8080/delete-user?ID=1
 
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /delete-user?ID=1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.0.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Content-Type: text/plain
< Date: Wed, 30 Oct 2024 12:50:02 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
$ curl -v -X POST localhost:8080/user?ID=1
 
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /user?ID=1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.0.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Content-Type: text/plain
< Date: Wed, 30 Oct 2024 12:50:24 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Webová služba tedy pracuje – minimálně z tohoto pohledu – korektně.

16. Nedostatky webové služby a jejich náprava v Go 1.22

I přes nepatrná vylepšení má naše webová služba mnoho nedostatků. Zejména se používá špatné pojmenování koncových bodů a taktéž při „adresování“ zdrojů (což jsou v našem případě osoby) se spíše používá zápis zdroje přímo do URL, tedy například ve stylu /users/1, /users/42 atd. Všechny tyto nedostatky je možné relativně snadno opravit právě v Go verze 1.22 nebo pochopitelně v jakékoli novější verzi Go. Při registraci handlerů je totiž možné specifikovat používanou HTTP metodu a navíc může adresa obsahovat měnitelné části, které se při konkrétním volání nahradí například právě ID konkrétní osoby. Konkrétně to znamená, že registraci handlerů můžeme zjednodušit na pouhé:

// REST API endpoints
http.HandleFunc("GET /users", s.returnListOfUsers)
http.HandleFunc("GET /users/{id}", s.returnOneUser)
http.HandleFunc("DELETE /users/{id}", s.deleteOneUser)

Pokud tedy webovou službu voláme a používáme zdánlivě stejný koncový bod /users, bude náš HTTP server nejdříve rozlišovat použitou metodu (tím především rozliší druhý a třetí handler). Taktéž se bude snažit zjistit, která adresa je nejvíce konkrétní, čímž odliší první handler od ostatních dvou. Výsledkem bude webová služba s korektně pojmenovanými koncovými body (i když je samozřejmě na delší diskusi, zda použít singulár či plurál; zde jsem zvolil druhou možnost).

To však ještě není vše, protože potřebujeme získat ID osoby, což bylo v předchozích dvou variantách relativně zdlouhavé. Nyní je situace mnohem lepší, protože můžeme zavolat metodu PathValue rozhraní http.Request. Tato metoda vrátí příslušnou proměnnou část adresy, a to formou řetězce (jeden řetězec, nikoli řez ani pole). Je tedy stále nutné provést konverzi řetězce na celé číslo, například tak, jak je to naznačeno v ukázkovém kódu:

func (s ServerImpl) returnOneUser(writer http.ResponseWriter, r *http.Request) {
        IDs := r.PathValue("id")
 
        ID, err := strconv.Atoi(IDs)
        if err != nil {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
        ...
        ...
        ...
 
}
Poznámka: proměnných části může být specifikováno více, ale musí mít unikátní jména:
        http.HandleFunc("GET /users", s.returnListOfUsers)
        http.HandleFunc("GET /users/{id}", s.returnOneUser)
        http.HandleFunc("DELETE /users/{id}", s.deleteOneUser)
        http.HandleFunc("GET /users/{id}/name/{name}/surname/{surname}", s.returnOneUserByNameSurname)
        http.HandleFunc("GET /users/{id}/name/{id}", s.returnOneUserByID)

Takto upravená služba sice bude přeložitelná, ale v čase běhu (runtime) zhavaruje kvůli poslednímu registrovanému handleru:

panic: parsing "GET /users/{id}/name/{id}": at offset 21: duplicate wildcard name "id"
 
goroutine 1 [running]:
net/http.(*ServeMux).register(...)
    /usr/local/go/src/net/http/server.go:2733
net/http.HandleFunc({0x6af8a5?, 0x0?}, 0x2?)
    /usr/local/go/src/net/http/server.go:2727 +0x86
main.ServerImpl.Serve({{0x722ef8?, 0xc00011c960?}}, 0x1f90)
    /home/ptisnovs/xy/api_service_5.go:94 +0x259
main.main()
    /home/ptisnovs/xy/api_service_5.go:151 +0x151
exit status 2

17. Úplný zdrojový kód čtvrté verze webové služby

Čtvrtá a současně i poslední varianta naší webové služby je již mnohem čitelnější a kratší, než varianty předchozí. Navíc mají koncové body korektní sémantiku:

package main
 
import (
        "encoding/json"
        "fmt"
        "log"
        "net/http"
        "strconv"
)
 
// Datova struktura predstavujici uzivatele
type User struct {
        Name    string
        Surname string
}
 
// Rozhrani s predpisem metod pro manipulace s databazi uzivatelu
type UserStorage interface {
        ReadListOfUsers() []User
        ReadUser(ID int) (User, bool)
        DeleteUser(ID int)
}
 
// Implementace databaze uzivatelu v operacni pameti
type MemoryStorage struct {
        users map[int]User
}
 
// Inicializace databaze uzivatelu
func NewMemoryStorage() MemoryStorage {
        users := make(map[int]User)
        users[1] = User{
                Name:    "Linus",
                Surname: "Torvalds",
        }
        users[2] = User{
                Name:    "Rob",
                Surname: "Pike",
        }
        users[3] = User{
                Name:    "Ken",
                Surname: "Iverson",
        }

        return MemoryStorage{
                users: users,
        }
}
 
// Implementace vsech metod predepsanych rozhranim UserStorage
 
func (s MemoryStorage) ReadListOfUsers() []User {
        users := make([]User, 0, len(s.users))
        for _, user := range s.users {
                users = append(users, user)
        }
        return users
}
 
func (s MemoryStorage) ReadUser(ID int) (User, bool) {
        user, found := s.users[ID]
        return user, found
}
 
func (s MemoryStorage) DeleteUser(ID int) {
        delete(s.users, ID)
}
 
// Rozhrani predepisujici metody serveru
type Server interface {
        Serve(port uint)
}
 
// Implementace HTTPServeru
type ServerImpl struct {
        storage UserStorage
}
 
// Inicialiace serveru, predani kontextu
func NewServer(storage UserStorage) Server {
        return ServerImpl{
                storage: storage,
        }
}
 
func (s ServerImpl) Serve(port uint) {
        log.Printf("Starting server on port 8080")
 
        // REST API endpoints
        http.HandleFunc("GET /users", s.returnListOfUsers)
        http.HandleFunc("GET /users/{id}", s.returnOneUser)
        http.HandleFunc("DELETE /users/{id}", s.deleteOneUser)
 
        // start the server
        http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}
 
// REST API handlery
 
func (s ServerImpl) returnListOfUsers(writer http.ResponseWriter, r *http.Request) {
        users := s.storage.ReadListOfUsers()
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(users)
}
 
func (s ServerImpl) returnOneUser(writer http.ResponseWriter, r *http.Request) {
        IDs := r.PathValue("id")
 
        ID, err := strconv.Atoi(IDs)
        if err != nil {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        user, found := s.storage.ReadUser(ID)
        if !found {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusNotFound)
                return
        }
        writer.Header().Set("Content-Type", "application/json")
        json.NewEncoder(writer).Encode(user)
}
 
func (s ServerImpl) deleteOneUser(writer http.ResponseWriter, r *http.Request) {
        IDs := r.PathValue("id")
 
        ID, err := strconv.Atoi(IDs)
        if err != nil {
                writer.Header().Set("Content-Type", "text/plain")
                writer.WriteHeader(http.StatusBadRequest)
                return
        }
 
        s.storage.DeleteUser(ID)
 
        writer.Header().Set("Content-Type", "application/json")
        status := struct {
                Status string
                ID     int
        }{"deleted", ID}
        json.NewEncoder(writer).Encode(status)
}
 
func main() {
        storage := NewMemoryStorage()
        server := NewServer(storage)
        server.Serve(8080)
}

18. Otestování základních funkcí poslední varianty webové služby

Nyní si již můžeme ověřit, jak pracuje (či naopak nepracuje) naše poslední varianta webové služby. Získání seznamu všech osob:

$ curl localhost:8080/users | jq .
 
[
  {
    "Name": "Linus",
    "Surname": "Torvalds"
  },
  {
    "Name": "Rob",
    "Surname": "Pike"
  },
  {
    "Name": "Ken",
    "Surname": "Iverson"
  }
]

Výběr (selekce) osoby na základě jejího ID a použití HTTP metody GET:

$ curl localhost:8080/users/1 | jq .
 
{
  "Name": "Linus",
  "Surname": "Torvalds"
}

Vymazání osoby na základě jejího ID a použití HTTP metody DELETE:

$ curl -X DELETE localhost:8080/users/1 
 
{"Status":"deleted","ID":1}

Seznam osob, nyní již obsahující pouze dva záznamy:

ict ve školství 24

$ curl localhost:8080/users | jq .
 
[
  {
    "Name": "Rob",
    "Surname": "Pike"
  },
  {
    "Name": "Ken",
    "Surname": "Iverson"
  }
]

Pokusy o použití nekorektní HTTP metody:

$ curl -X PUT localhost:8080/users
 
Method Not Allowed
 
 
$ curl -X POST localhost:8080/users
 
Method Not Allowed
 
 
$ curl -X DELETE localhost:8080/users
 
Method Not Allowed

19. 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ář, můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následujících tabulkách.

Klasická implementace HTTP serverů založených pouze na základních knihovnách jazyka Go

# Soubor Popis Cesta
1 01_server.go jednoduchý HTTP server posílající dynamicky generovaný obsah https://github.com/tisnik/go-root/blob/master/article24/01_ser­ver.go
2 02_slow_server.go zpomalení generování jednotlivých bloků generovaného obsahu https://github.com/tisnik/go-root/blob/master/article24/02_slow_ser­ver.go
3 03_flushing_server.go využití metody Flush z rozhraní Flusher https://github.com/tisnik/go-root/blob/master/article24/03_flushin­g_server.go
4 04_close_detector.go test, zda klient neukončil spojení https://github.com/tisnik/go-root/blob/master/article24/04_clo­se_detector.go

HTTP servery založené na knihovně Gorilla mux

# Příklad Stručný popis Cesta
1 01_simple_http_server.go jednoduchý HTTP server založený na standardním balíčku net/http https://github.com/tisnik/go-root/blob/master/article38/01_sim­ple_http_server.go
2 02_http_server_with_mux.go HTTP server používající balíček gorilla/mux https://github.com/tisnik/go-root/blob/master/article38/02_http_ser­ver_with_mux.go
3 03_method_specification.go specifikace HTTP metod použitých při volání REST API https://github.com/tisnik/go-root/blob/master/article38/03_met­hod_specification.go
4 04_method_specification.go rozšíření a vylepšení předchozího příkladu https://github.com/tisnik/go-root/blob/master/article38/04_met­hod_specification.go
5 05_resource_handling.go vylepšená správa zdrojů (resources) https://github.com/tisnik/go-root/blob/master/article38/05_re­source_handling.go
6 06_resource_handling.go práce se jménem zdroje https://github.com/tisnik/go-root/blob/master/article38/06_re­source_handling.go
7 07_resource_handling_id.go omezení znaků, které může být použito ve jménu zdroje https://github.com/tisnik/go-root/blob/master/article38/07_re­source_handling_id.go
8 08_headers.go specifikace hlaviček, které musí být poslány společně s požadavkem https://github.com/tisnik/go-root/blob/master/article38/08_he­aders.go
9 09_subroutes.go vytvoření a konfigurace podsměrovačů (subroutes) https://github.com/tisnik/go-root/blob/master/article38/09_su­broutes.go
10 10_simple_middleware.go dvě middleware funkce použité při zpracování dotazů https://github.com/tisnik/go-root/blob/master/article38/10_sim­ple_middleware.go

HTTP servery založené na základních knihovnách jazyka Go verze 1.22 a vyšších

# Soubor Popis Cesta
1 api_service1.go základní struktura webové služby, zde jen s jediným koncovým bodem https://github.com/tisnik/go-root/blob/master/article_A­E/api_service1.go
2 api_service2.go přidání handlerů pro další koncové body (ovšem bez korektní sémantiky) https://github.com/tisnik/go-root/blob/master/article_A­E/api_service2.go
3 api_service3.go test, zda je použita korektní HTTP metoda https://github.com/tisnik/go-root/blob/master/article_A­E/api_service3.go
4 api_service4.go korektní sémantika koncových bodů i jejich handlerů https://github.com/tisnik/go-root/blob/master/article_A­E/api_service4.go
5 api_service5.go duplikátní proměnné části při registraci koncových bodů https://github.com/tisnik/go-root/blob/master/article_A­E/api_service5.go

20. Odkazy na Internetu

  1. Go 1.22 Release Notes
    https://go.dev/doc/go1.22#en­hanced_routing_patterns
  2. Dokumentace k balíčku gorilla/mux
    https://godoc.org/github.com/go­rilla/mux
  3. Gorilla web toolkitk
    http://www.gorillatoolkit.org/
  4. Metric types
    https://prometheus.io/doc­s/concepts/metric_types/
  5. Histograms with Prometheus: A Tale of Woe
    http://linuxczar.net/blog/2017/06/15/pro­metheus-histogram-2/
  6. Instrumenting Golang server in 5 min
    https://medium.com/@gsisi­mogang/instrumenting-golang-server-in-5-min-c1c32489add3
  7. Routing Enhancements for Go 1.22
    https://go.dev/blog/routing-enhancements
  8. net/http: enhanced ServeMux routing #61410
    https://github.com/golang/go/is­sues/61410
  9. net/http: add methods and path variables to ServeMux patterns #60227
    https://github.com/golang/go/dis­cussions/60227
  10. curl man page
    https://curl.se/docs/manpage.html

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.