Go HTTP web server návod časť 2 - dynamický rounting

Sunday, Sep 6, 2020 by Nelo

Go ponúka jednoduchý router, ale čo ak nám nestačí statický routing a potrebujeme smerovať požiadavky napríklad podľa čísla zákazníka, článku alebo dátumu? Štandardný routing môžeme jednoducho rozšíriť o regulárne výrazy a ďalej napríklad kvôli bezpečnosti kontrolovať HTTP metódu. Na väčšinu aplikácií postačí základná funkcionalita a nie su nutné frameworky, ak však hľadáte sofistikované routing balíčky odporúčam pozrieť Gorilla/Mux alebo Go-Chi.

Dynamický router v Go

Dnes si vytvoríme jednoduchý dynamický router s rule enginom, ktorý traverzuje stromom pravidiel a hľadá zhodu pomocou regulárnych výrazov. Okrem regulárnych výrazov použjeme vlastnosti Go ako slice, map, first class function a function receiver. Rule engine je veľmi jednoduchý, ale flexibilný a je ľahké do neho doplniť ďalšiu funkcionalitu ako napríklad middleware, kontrolu autorizácie atď.

Endpointy:

  • /customer/NNNN: zavolá metódu s parametrom identifikačného čísla zákazníka, GET alebo POST metóda
  • /YYYY-MM-DD: zavolá metódu s parametrom dátumu, len GET metóda (nekontroluje platnosť dátumu)

Výstupy:

$ curl -X GET http://localhost:8000/customer/
/customer/
Matched [Root]
Matched [GET]
Handled [GET] in 0.000 seconds
Handled [Root] in 0.000 seconds
Processing successful: FALSE

$ curl -X GET http://localhost:8000/customer/123/abc?call
/customer/123/abc
Matched [Root]
Matched [GET]
Matched [Customer]
customer = 123
Handled [Customer] in 0.010 seconds
Handled [GET] in 0.010 seconds
Handled [Root] in 0.010 seconds
Processing successful: TRUE

$ curl -X GET http://localhost:8000/2016-04-15
/2016-04-15
Matched [Root]
Matched [GET]
Matched [Date]
map[Day:15 Month:04 Year:2016]
Handled [Date] in 0.011 seconds
Handled [GET] in 0.011 seconds
Handled [Root] in 0.011 seconds
Processing successful: TRUE

$ curl -X POST http://localhost:8000/customer/456
/customer/456
Matched [Root]
Matched [POST]
Matched [Customer]
customer = 456
Handled [Customer] in 0.010 seconds
Handled [POST] in 0.010 seconds
Handled [Root] in 0.010 seconds
Processing successful: TRUE

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

Dátové štruktúry

Najskôr si zadefinujme dátovú štruktúru Rule, ktorá bude tvoriť uzol v strome routingových pravidiel. Bude mať meno (Name) na identifikáciu a child objekty (Children slice). Vďaka tomu, že Go podporuje “First Class Functions”, môžme zadefinovať premenné Match a Handler typu funkcie, ktorým implementáciu priradíme neskôr.

Ďalej doplníme k Rule štruktúre “receiver” funkciu Process(), ktorá najskôr otestuje zhodu pravidiel zavolaním Match() a ak nastane zhoda zavolá Process() v Children uzloch pokým nenarazí na zhodu a nakoniec zavolá Handler() funkciu. time.Sleep() je volaný len aby simuloval pomalšie spravovanie pre zmysluplnejší výpis dĺžky spracovania.

Ak vás zaujíma implementácia vlastných Tree štruktúr v Go, odporúčam článok Golang Datastructures: Trees.

 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
// Rule tree with name string and children slice with Match() and Handler() method
type Rule struct {
	Name     string
	Children []Rule
	Match    func(r *http.Request) bool
	Handler  func(w http.ResponseWriter, r *http.Request)
}

// Process traverses through route tree structure until it finds matching leaf
func (n Rule) Process(w http.ResponseWriter, r *http.Request) bool {
	startTime := time.Now()
	found := false
	if n.Match != nil && n.Match(r) == true {
		found = true
		fmt.Fprintln(w, "Matched ["+n.Name+"]")
		for _, v := range n.Children {
			found = false
			if v.Process(w, r) {
				found = true
				break
			}
		}
		if n.Handler != nil {
			n.Handler(w, r)
			time.Sleep(10 * time.Millisecond)
		}
		fmt.Fprintln(w, "Handled ["+n.Name+"] in "+fmt.Sprintf("%.3f seconds", time.Since(startTime).Seconds()))
	}
	return found
}

Pravidlá

Pravidlá zadefinujeme cez regulárne výrazy a otestovať si ich môžete na stránke, ktorá podporuje Go formát vrátane capture group a testovania Regex101:

1
2
3
4
var (
	rCustomer = regexp.MustCompile(`^/customer/(?P<customer>[\d]+)(/|$)`)               // Contains "/customer/123/*"
	rDate     = regexp.MustCompile(`^/(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})`) // Contains "/2020-03-21"
)

Customer a Date handler

Tieto dve funkcie len vyextrahujú pomocou regulárnych výrazov ID zákazníka, respektíve dátum a vrátia volané hodnoty:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func customerHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "customer = "+rCustomer.FindStringSubmatch(r.URL.Path)[1])
}

func dateHandler(w http.ResponseWriter, r *http.Request) {
	keys := rDate.SubexpNames()[1:]
	values := rDate.FindStringSubmatch(r.URL.Path)[1:]
	elementMap := make(map[string]string)
	for i, k := range keys {
		elementMap[k] = values[i]
	}
	fmt.Fprintln(w, elementMap)
}

Main funkcia

V hlavnej časti programu si najskôr zadefinujeme strom s uzlami, pravidlami a handlermi. Keďže o vyhodnotenie sa rekurzívne stará funkcia Process(), v http.HandleFunc() voláme len Process() koreňového uzlu a spustí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
func main() {
	dateRule := Rule{
		Name:    "Date",
		Match:   func(r *http.Request) bool { return rDate.MatchString(r.URL.Path) },
		Handler: dateHandler}
	customerRule := Rule{
		Name:    "Customer",
		Match:   func(r *http.Request) bool { return rCustomer.MatchString(r.URL.Path) },
		Handler: customerHandler}
	getRule := Rule{
		Name:     "GET",
		Match:    func(r *http.Request) bool { return r.Method == "GET" },
		Children: []Rule{customerRule, dateRule}}
	postRule := Rule{
		Name:     "POST",
		Match:    func(r *http.Request) bool { return r.Method == "POST" },
		Children: []Rule{customerRule}}
	root := Rule{
		Name:     "Root",
		Match:    func(r *http.Request) bool { return true },
		Children: []Rule{getRule, postRule}}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, r.URL.Path)
		found := root.Process(w, r)
		fmt.Fprintln(w, "Processing successful: "+strings.ToUpper(strconv.FormatBool(found)))
	})

	log.Fatal(http.ListenAndServe(":8000", 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”:

 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
package main

import (
	"fmt"
	"log"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// Rule tree with name string and children slice with Match() and Handler() method
type Rule struct {
	Name     string
	Children []Rule
	Match    func(r *http.Request) bool
	Handler  func(w http.ResponseWriter, r *http.Request)
}

// Process traverses through route tree structure until it finds matching leaf
func (n Rule) Process(w http.ResponseWriter, r *http.Request) bool {
	startTime := time.Now()
	found := false
	if n.Match != nil && n.Match(r) == true {
		found = true
		fmt.Fprintln(w, "Matched ["+n.Name+"]")
		for _, v := range n.Children {
			found = false
			if v.Process(w, r) {
				found = true
				break
			}
		}
		if n.Handler != nil {
			n.Handler(w, r)
			time.Sleep(10 * time.Millisecond)
		}
		fmt.Fprintln(w, "Handled ["+n.Name+"] in "+fmt.Sprintf("%.3f seconds", time.Since(startTime).Seconds()))
	}
	return found
}

var (
	rCustomer = regexp.MustCompile(`^/customer/(?P<customer>[\d]+)(/|$)`)               // Contains "/customer/123/*"
	rDate     = regexp.MustCompile(`^/(?P<Year>\d{4})-(?P<Month>\d{2})-(?P<Day>\d{2})`) // Contains "/2020-03-21"
)

func customerHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "customer = "+rCustomer.FindStringSubmatch(r.URL.Path)[1])
}

func dateHandler(w http.ResponseWriter, r *http.Request) {
	keys := rDate.SubexpNames()[1:]
	values := rDate.FindStringSubmatch(r.URL.Path)[1:]
	elementMap := make(map[string]string)
	for i, k := range keys {
		elementMap[k] = values[i]
	}
	fmt.Fprintln(w, elementMap)
}

func main() {
	dateRule := Rule{
		Name:    "Date",
		Match:   func(r *http.Request) bool { return rDate.MatchString(r.URL.Path) },
		Handler: dateHandler}
	customerRule := Rule{
		Name:    "Customer",
		Match:   func(r *http.Request) bool { return rCustomer.MatchString(r.URL.Path) },
		Handler: customerHandler}
	getRule := Rule{
		Name:     "GET",
		Match:    func(r *http.Request) bool { return r.Method == "GET" },
		Children: []Rule{customerRule, dateRule}}
	postRule := Rule{
		Name:     "POST",
		Match:    func(r *http.Request) bool { return r.Method == "POST" },
		Children: []Rule{customerRule}}
	root := Rule{
		Name:     "Root",
		Match:    func(r *http.Request) bool { return true },
		Children: []Rule{getRule, postRule}}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, r.URL.Path)
		found := root.Process(w, r)
		fmt.Fprintln(w, "Processing successful: "+strings.ToUpper(strconv.FormatBool(found)))
	})

	log.Fatal(http.ListenAndServe(":8000", 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