Keď potrebujeme aby dve alebo viac aplikácií medzi sebou komunikovali, rozhodujeme sa akú API architektúru použijeme. Synchrónnu (request-response) alebo asynchrónnu (message/event-driven/streaming) s akým protokolom. Synchrónna sa najčastejšie používa pri klient-server architektúrach ak sa častejšie dopytujeme a požadujeme výsledok. Asynchrónna sa používa ak k nám častejšie informácie prichádzajú (počúvanie na udalosti, streamovanie hudby), alebo ich spracovanie trvá príliš dlho, alebo máme viacerých producentov alebo konzumerov správ.
REST (Representational State Transfer) definovaný v roku 2000 dodnes patrí medzi najpoužívanejšiu formu synchrónnej komunikácie a API. Medzi ďalšie populárne API patria starší SOAP (1999), GraphQL (2015) a gRPC (2016).
V tomto článku si naprogramujeme jednoduchý Go HTTP REST API server. Povieme si prečo je dôležité API verzionovať a aké existujú verzionovacie stratégie. Ďalej doplníme health check endpoint podľa RFC štandardu, ktorý umožní monitorovacím nástrojom zbierať metriky. Pritom si ukážeme ako pracovať s headerom, nastaviť content-type a kontrolu cache.
API endpointy
Predpoklady
- Nainštalované Go prostredie
- (odporúčané) Go IDE, napríklad Visual Studio Code alebo JetBrains Goland
Limitácie
Tento jednoduchý ilustračný príklad má niekoľko limitácií. Ako riešiť tieto limitácie sa pozrieme v ďalších dieloch návodov.
- Obmedzený výkon, nakoľko nie sú použité gorutiny a kanále
- Obmedzená rozšíriteľnosť, nepoužíva externú konfiguráciu, nezapuzdruje funkcionality do objektov, modulov a balíčkov
- Nerieši logovanie, bezpečnosť, testovanie
- Routing, nakoľko používa jednoduchý statický routing a nerozlišuje GET, POST volania
Dátové štruktúry
Najskôr si zadefinujme typy objektov, ktoré budeme používať:
- appInfo: informácia o aplikácii a odpoveď na volanie /version
- appV1Result: odpoveď na volanie /app/1
- metric: jednoduchá metrika, ktorá bude obsahovať uptime
- healthDetails: kolekcia metrík, v našom príklade bude obsahovať iba metriku uptime
- health: odpoveď na volanie /health
- konštanty: názov servisu, verzia, port
- premenné: čas spustenia servera
V Go, štruktúra je spôsob ako zadefinovať konkrétne užívateľské typy. Štruktúra v Go je kolekcia polí alebo vlastností. Narozdiel od iných programovacích jazykov Go neponúka kľúčové slovo “class”, štruktúra je odľahčená verzia tried.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
type appInfo struct {
Service string `json:"service"`
Major int `json:"major"`
Minor int `json:"minor"`
Patch int `json:"patch"`
}
type appV1Result struct {
Result string `json:"result"`
}
type metric struct {
Status string `json:"status"`
ObservedValue string `json:"observedValue"`
ObservedUnit string `json:"observedUnit"`
Time string `json:"time"`
}
type healthDetails struct {
Uptime []metric `json:"uptime"`
}
type health struct {
Status string `json:"status"`
Version string `json:"version"`
ReleaseId string `json:"releaseId"`
Description string `json:"description"`
Details healthDetails `json:"details"`
}
const (
serviceName = "simple api server"
majorVersion = 1
minorVersion = 0
patchVersion = 0
port = 8000
)
var (
startTime time.Time
)
|
Index handler
Zavolá sa pri ceste “/” alebo akejkoľvek inej nedefinovanej ceste. Cesta “/” vráti text “Hello from the other world”. Nedefinovaná “/api*” cesta vráti 501 Not Implemented. Akákoľvek iná nedefinovaná cesta vráti 404 Not Found.
1
2
3
4
5
6
7
8
9
10
11
|
func indexHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
if strings.HasPrefix(r.URL.Path, "/api") {
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
return
}
http.NotFound(w, r)
return
}
fmt.Fprintf(w, "Hello from the other world")
}
|
Version handler
Vytvorí JSON objekt s názvom servisu a verziou. Go balíček “encoding/json” obsahuje fukciu Marshal, ktorá traverzuje štruktúru a skonvertuje ju do UTF8 JSON []byte. Kódovanie názvov je možné upraviť “json” tagmi v poliach štruktúry.
1
2
3
4
5
6
7
8
|
func versionHandler(w http.ResponseWriter, r *http.Request) {
jsonStr, err := json.Marshal(appInfo{Service: serviceName, Major: majorVersion, Minor: minorVersion, Patch: patchVersion})
if err != nil {
fmt.Fprintf(w, "Error: %s", err)
return
}
w.Write(jsonStr)
}
|
API handler
Volanie “/api/1” vracajúce JSON odpoveď {Result: “OK”}. V hlavičke odpovedi nastavíme content-type a cache-control.
1
2
3
4
5
6
7
8
9
10
11
|
func apiV1Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Add("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
jsonStr, err := json.Marshal(appV1Result{Result: "OK"})
if err != nil {
fmt.Fprintf(w, "Error: %s", err)
return
}
w.Write(jsonStr)
}
|
Health check handler
Health check zozbiera metriky, napríklad uptime a vráti JSON so štandardizovaným health check výstupom:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
{
"status": "pass",
"version": "1",
"releaseId": "1.0.0",
"description": "simple api server",
"details": {
"uptime": [
{
"status": "pass",
"observedValue": "2.014",
"observedUnit": "seconds",
"time": "2020-08-31 16:16:39.3876516 +0200 CEST m=+2.015000901"
}
]
}
}
|
Numerické hodnoty v health štruktúre je potrebné skonvertovať do stringu buď funkciou strconv.Itoa() alebo fmt.Sprintf(), časové hodnoty skonvertujeme Time.String().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/health+json")
w.Header().Add("Cache-Control", "max-age=3600")
uptime := metric{
Status: "pass",
ObservedUnit: "seconds",
ObservedValue: fmt.Sprintf("%.3f", time.Since(startTime).Seconds()),
Time: time.Now().String()}
health := health{
Status: "pass",
Description: serviceName,
Version: strconv.Itoa(majorVersion),
ReleaseId: fmt.Sprintf("%d.%d.%d", majorVersion, minorVersion, patchVersion),
Details: healthDetails{Uptime: []metric{uptime}}}
jsonStr, err := json.Marshal(health)
if err != nil {
fmt.Fprintf(w, "Error: %s", err)
return
}
w.Write(jsonStr)
}
|
Main funkcia
V main funkcii nastavíme čas spustenia servera a jednotlivé handler funkcie. Následne spustíme server počúvajúci na definovanom porte. Viac o balíčku net/http.
1
2
3
4
5
6
7
8
|
func main() {
startTime = time.Now()
http.HandleFunc("/", indexHandler)
http.HandleFunc("/version", versionHandler)
http.HandleFunc("/api/1", apiV1Handler)
http.HandleFunc("/health", healthHandler)
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil))
}
|
Celý zdrojový kód
Zdrojový kód môžete uložiť do súboru “main.go” a spustiť príkazom “go run main.go” a otvoríme server (http://localhost:8000):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
)
type appInfo struct {
Service string `json:"service"`
Major int `json:"major"`
Minor int `json:"minor"`
Patch int `json:"patch"`
}
type appV1Result struct {
Result string `json:"result"`
}
type metric struct {
Status string `json:"status"`
ObservedValue string `json:"observedValue"`
ObservedUnit string `json:"observedUnit"`
Time string `json:"time"`
}
type healthDetails struct {
Uptime []metric `json:"uptime"`
}
type health struct {
Status string `json:"status"`
Version string `json:"version"`
ReleaseId string `json:"releaseId"`
Description string `json:"description"`
Details healthDetails `json:"details"`
}
const (
serviceName = "simple api server"
majorVersion = 1
minorVersion = 0
patchVersion = 0
port = 8000
)
var (
startTime time.Time
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
if strings.HasPrefix(r.URL.Path, "/api") {
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
return
}
http.NotFound(w, r)
return
}
fmt.Fprintf(w, "Hello from the other world")
}
func versionHandler(w http.ResponseWriter, r *http.Request) {
jsonStr, err := json.Marshal(appInfo{Service: serviceName, Major: majorVersion, Minor: minorVersion, Patch: patchVersion})
if err != nil {
fmt.Fprintf(w, "Error: %s", err)
return
}
w.Write(jsonStr)
}
func apiV1Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Add("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
jsonStr, err := json.Marshal(appV1Result{Result: "OK"})
if err != nil {
fmt.Fprintf(w, "Error: %s", err)
return
}
w.Write(jsonStr)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/health+json")
w.Header().Add("Cache-Control", "max-age=3600")
uptime := metric{
Status: "pass",
ObservedUnit: "seconds",
ObservedValue: fmt.Sprintf("%.3f", time.Since(startTime).Seconds()),
Time: time.Now().String()}
health := health{
Status: "pass",
Description: serviceName,
Version: strconv.Itoa(majorVersion),
ReleaseId: fmt.Sprintf("%d.%d.%d", majorVersion, minorVersion, patchVersion),
Details: healthDetails{Uptime: []metric{uptime}}}
jsonStr, err := json.Marshal(health)
if err != nil {
fmt.Fprintf(w, "Error: %s", err)
return
}
w.Write(jsonStr)
}
func main() {
startTime = time.Now()
http.HandleFunc("/", indexHandler)
http.HandleFunc("/version", versionHandler)
http.HandleFunc("/api/1", apiV1Handler)
http.HandleFunc("/health", healthHandler)
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil))
}
|
Referencie
Vaše otázky, návrhy a komentáre
Verím, že vás tento návod inšpiroval a budem vďačný ak dáte spätnú väzbu a pomôže mi zamerať sa na to čo by vás zaujímalo.