Héctor
Héctor

Reputation: 26034

Go: Get path parameters from http.Request

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

Answers (7)

Adrian Miranda
Adrian Miranda

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

SteppingHat
SteppingHat

Reputation: 1362

Go 1.22 has now introduced this functionality natively in 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

Awacate
Awacate

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

Benjamin Carlsson
Benjamin Carlsson

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

Leonid Pavlov
Leonid Pavlov

Reputation: 809

Golang read value from URL query "/template/get/123".

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)
}

Golang read value from URL query "/template/get?id=123".

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

nipuna
nipuna

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

JimB
JimB

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

Related Questions