Reputation: 26034
I'm developing a REST API with Go, but I don't know how can I do the path mappings and retrieve the path parameters from them.
I want something like this:
func main() {
http.HandleFunc("/provisions/:id", Provisions) //<-- How can I map "id" parameter in the path?
http.ListenAndServe(":8080", nil)
}
func Provisions(w http.ResponseWriter, r *http.Request) {
//I want to retrieve here "id" parameter from request
}
I would like to use just http
package instead of web frameworks, if it is possible.
Thanks.
Upvotes: 66
Views: 97245
Reputation: 334
I'm still learning Go (Golang). I don't know if there is an easier way to do this, but that's my solution:
package router
import "regexp"
func ExtractParamsFromPattern(path, pattern string) map[string]string {
params := make(map[string]string)
if path == "" || pattern == "" {
return params
}
paramPattern := `[a-zA-Z0-9_:]`
paramPatternGroup := "(" + paramPattern + "{0,})"
paramPatternExp := ":" + paramPattern + "+"
reParamExp := regexp.MustCompile(paramPatternExp)
rePatternValues := "^" + reParamExp.ReplaceAllString(pattern, paramPatternGroup)
reParamValues := regexp.MustCompile(rePatternValues)
var paramNames []string
keys := reParamExp.FindAllStringSubmatch(pattern, -1)
for _, paramName := range keys {
paramNames = append(paramNames, paramName[0][1:])
}
var paramValues []string
values := reParamValues.FindAllStringSubmatch(path, -1)
if len(values) > 0 {
paramValues = values[0][1:]
}
for id, name := range paramNames {
if id < len(paramValues) {
params[name] = paramValues[id]
} else {
params[name] = ""
}
}
return params
}
package router
import "testing"
func TestExtractParamsFromPattern(t *testing.T) {
tests := []struct {
name string
path string
pattern string
expected map[string]string
}{
{
name: "Teste 1: Correspondência exata com um único parâmetro",
path: "/users/123",
pattern: "/users/:id",
expected: map[string]string{
"id": "123",
},
},
{
name: "Teste 2: Correspondência exata com múltiplos parâmetros",
path: "/posts/123/comments/456",
pattern: "/posts/:postId/comments/:commentId",
expected: map[string]string{
"postId": "123",
"commentId": "456",
},
},
{
name: "Teste 3: Quando o caminho não corresponde ao padrão",
path: "/posts/123/comments/456",
pattern: "/users/:userId",
expected: map[string]string{
"userId": "",
},
},
{
name: "Teste 4: Quando o caminho ou padrão estão vazios",
path: "",
pattern: "/users/:id",
expected: map[string]string{},
},
{
name: "Teste 4-1: Quando o caminho ou padrão estão vazios",
path: "/users/123",
pattern: "",
expected: map[string]string{},
},
{
name: "Teste 4-2: Quando o caminho ou padrão estão vazios",
path: "",
pattern: "",
expected: map[string]string{},
},
{
name: "Teste 5: Correspondência sem parâmetros, só o caminho",
path: "/about",
pattern: "/about",
expected: map[string]string{},
},
{
name: "Teste 6: Parâmetros alfanuméricos",
path: "/engines/abc123",
pattern: "/engines/:id",
expected: map[string]string{
"id": "abc123",
},
},
{
name: "Teste 7: Parâmetros com caracteres especiais que devem ser ignorados",
path: "/products/abc123?type=book",
pattern: "/products/:id",
expected: map[string]string{
"id": "abc123",
},
},
{
name: "Teste 7-1: Parâmetros com caracteres especiais que devem ser ignorados",
path: "/products/xablau//",
pattern: "/products/:id/:teste/:ae",
expected: map[string]string{
"id": "xablau",
"teste": "",
"ae": "",
},
},
{
name: "Teste 8: Correspondência com múltiplos parâmetros no padrão",
path: "/users/42/products/123",
pattern: "/users/:userId/products/:productId",
expected: map[string]string{
"userId": "42",
"productId": "123",
},
},
{
name: "Teste 9: Quando o padrão não tem parâmetros",
path: "/about",
pattern: "/about",
expected: map[string]string{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := ExtractParamsFromPattern(test.path, test.pattern)
if len(result) != len(test.expected) {
t.Errorf("Esperado %v, obtido %v", test.expected, result)
return
}
for key, expectedValue := range test.expected {
if result[key] != expectedValue {
t.Errorf("Para a chave '%s', esperado '%s', mas obtido '%s'", key, expectedValue, result[key])
}
}
})
}
}
and/or:
package router
import (
"fmt"
"regexp"
)
func SubstituteParams(pattern string, params map[string]interface{}) string {
re := regexp.MustCompile(`:([a-zA-Z0-9_]+)`)
substitutedURL := re.ReplaceAllStringFunc(pattern, func(match string) string {
param := match[1:]
value, exists := params[param]
if !exists {
return match
}
return fmt.Sprintf("%v", value)
})
return substitutedURL
}
package router
import "testing"
func TestSubstituteParams(t *testing.T) {
tests := []struct {
name string
pattern string
params map[string]interface{}
expected string
}{
{
name: "Substituição simples de parâmetros na URL",
pattern: "/products/:id/:category",
params: map[string]interface{}{
"id": 123,
"category": "electronics",
},
expected: "/products/123/electronics",
},
{
name: "Parâmetro faltante: substitui apenas o que existe",
pattern: "/products/:id/:category",
params: map[string]interface{}{
"id": 123,
},
expected: "/products/123/:category",
},
{
name: "Parâmetros extras são ignorados na substituição",
pattern: "/products/:id/:category",
params: map[string]interface{}{
"id": 123,
"category": "electronics",
"color": "red",
},
expected: "/products/123/electronics",
},
{
name: "Parâmetro ausente: substitui o existente e deixa vazio o faltante",
pattern: "/products/:id/:category",
params: map[string]interface{}{
"id": 123,
},
expected: "/products/123/:category",
},
{
name: "Sem parâmetros na URL, retorna a URL original",
pattern: "/products",
params: map[string]interface{}{},
expected: "/products",
},
{
name: "Sem parâmetros fornecidos: não realiza substituição",
pattern: "/products/:id/:category",
params: map[string]interface{}{},
expected: "/products/:id/:category",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SubstituteParams(tt.pattern, tt.params)
if result != tt.expected {
t.Errorf("Esperado %v, obtido %v", tt.expected, result)
}
})
}
}
Upvotes: 0
Reputation: 1362
net/http
This can now be achieved by calling req.PathValue()
PathValue returns the value for the named path wildcard in the ServeMux pattern that matched the request. It returns the empty string if the request was not matched against a pattern or there is no such wildcard in the pattern.
A basic example on how to use reqPathValue()
is:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/provisions/{id}", func(w http.ResponseWriter, req *http.Request) {
idString := req.PathValue("id")
fmt.Printf("ID: %v", idString)
})
log.Fatal(http.ListenAndServe(":8080", mux))
}
Upvotes: 11
Reputation: 669
If you are using Go 1.22, you can easily retrieve the path parameters with r.PathValue()
. For example:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/provisions/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Provision ID: ", r.PathValue("id"))
})
mux.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/provisions/123", nil))
mux.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/provisions/456", nil))
}
Demo in Go Playground. Source: https://pkg.go.dev/net/http#Request.PathValue
Upvotes: 38
Reputation: 610
You can do this with standard library handlers. Note http.StripPrefix
accepts an http.Handler
and returns a new one:
func main() {
mux := http.NewServeMux()
provisionsPath := "/provisions/"
mux.Handle(
provisionsPath,
http.StripPrefix(provisionsPath, http.HandlerFunc(Provisions)),
)
}
func Provisions(w http.ResponseWriter, r *http.Request) {
fmt.Println("Provision ID:", r.URL.Path)
}
See working demo on Go playground.
You can also nest this behavior using submuxes; http.ServeMux
implements http.Handler
, so you can pass one into http.StripPrefix
just the same.
Upvotes: 3
Reputation: 809
I used standard gin routing and handling request parameters from context parameters.
Use this in registering your endpoint:
func main() {
r := gin.Default()
g := r.Group("/api")
{
g.GET("/template/get/:Id", templates.TemplateGetIdHandler)
}
r.Run()
}
And use this function in handler
func TemplateGetIdHandler(c *gin.Context) {
req := getParam(c, "Id")
//your stuff
}
func getParam(c *gin.Context, paramName string) string {
return c.Params.ByName(paramName)
}
Use this in registering your endpoint:
func main() {
r := gin.Default()
g := r.Group("/api")
{
g.GET("/template/get", templates.TemplateGetIdHandler)
}
r.Run()
}
And use this function in handler
type TemplateRequest struct {
Id string `form:"id"`
}
func TemplateGetIdHandler(c *gin.Context) {
var request TemplateRequest
err := c.Bind(&request)
if err != nil {
log.Println(err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
//your stuff
}
Upvotes: 3
Reputation: 4095
You can use golang gorilla/mux package's router to do the path mappings and retrieve the path parameters from them.
import (
"fmt"
"github.com/gorilla/mux"
"net/http"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/provisions/{id}", Provisions)
http.ListenAndServe(":8080", r)
}
func Provisions(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, ok := vars["id"]
if !ok {
fmt.Println("id is missing in parameters")
}
fmt.Println(`id := `, id)
//call http://localhost:8080/provisions/someId in your browser
//Output : id := someId
}
Upvotes: 23
Reputation: 109332
If you don't want to use any of the multitude of the available routing packages, then you need to parse the path yourself:
Route the /provisions path to your handler
http.HandleFunc("/provisions/", Provisions)
Then split up the path as needed in the handler
id := strings.TrimPrefix(req.URL.Path, "/provisions/")
// or use strings.Split, or use regexp, etc.
Upvotes: 80