Go HTTP web server návod časť 1

Monday, Aug 31, 2020 by Nelo

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

  1. Nainštalované Go prostredie
  2. (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.

  1. Obmedzený výkon, nakoľko nie sú použité gorutiny a kanále
  2. Obmedzená rozšíriteľnosť, nepoužíva externú konfiguráciu, nezapuzdruje funkcionality do objektov, modulov a balíčkov
  3. Nerieši logovanie, bezpečnosť, testovanie
  4. 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.

TAK ČO HOVORÍŠ ?

Kontaktuj nás ak potrebuješ pomoc, veľmi radi pomôžeme.

Kontaktuj nás