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
- 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
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.