param slice buffer
This commit is contained in:
parent
5be8eac6d8
commit
e4abb99df9
155
DOCS.md
Normal file
155
DOCS.md
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# Router Package Documentation
|
||||||
|
|
||||||
|
A fast, lightweight HTTP router for Go with support for middleware, route groups, and path parameters.
|
||||||
|
|
||||||
|
## Core Types
|
||||||
|
|
||||||
|
### Router
|
||||||
|
|
||||||
|
Main router that implements `http.Handler`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
router := router.New()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handler
|
||||||
|
|
||||||
|
Request handler function type.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Handler func(w http.ResponseWriter, r *http.Request, params []string)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
Function type for middleware.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Middleware func(Handler) Handler
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group
|
||||||
|
|
||||||
|
Route group with a prefix.
|
||||||
|
|
||||||
|
```go
|
||||||
|
group := router.Group("/api")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Router Methods
|
||||||
|
|
||||||
|
### New()
|
||||||
|
|
||||||
|
Creates a new router.
|
||||||
|
|
||||||
|
```go
|
||||||
|
router := router.New()
|
||||||
|
```
|
||||||
|
|
||||||
|
### ServeHTTP(w, r)
|
||||||
|
|
||||||
|
Implements `http.Handler` interface.
|
||||||
|
|
||||||
|
### Use(mw ...Middleware)
|
||||||
|
|
||||||
|
Adds global middleware.
|
||||||
|
|
||||||
|
```go
|
||||||
|
router.Use(loggingMiddleware, authMiddleware)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handle(method, path, handler)
|
||||||
|
|
||||||
|
Registers a handler for the given method and path.
|
||||||
|
|
||||||
|
```go
|
||||||
|
router.Handle("GET", "/users", listUsersHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Method Shortcuts
|
||||||
|
|
||||||
|
```go
|
||||||
|
router.Get("/users", listUsersHandler)
|
||||||
|
router.Post("/users", createUserHandler)
|
||||||
|
router.Put("/users/[id]", updateUserHandler)
|
||||||
|
router.Patch("/users/[id]", patchUserHandler)
|
||||||
|
router.Delete("/users/[id]", deleteUserHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group(prefix)
|
||||||
|
|
||||||
|
Creates a route group with prefix.
|
||||||
|
|
||||||
|
```go
|
||||||
|
api := router.Group("/api")
|
||||||
|
```
|
||||||
|
|
||||||
|
### WithMiddleware(mw ...Middleware)
|
||||||
|
|
||||||
|
Applies middleware to the next route registration.
|
||||||
|
|
||||||
|
```go
|
||||||
|
router.WithMiddleware(authMiddleware).Get("/admin", adminHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Group Methods
|
||||||
|
|
||||||
|
### Use(mw ...Middleware)
|
||||||
|
|
||||||
|
Adds middleware to the group.
|
||||||
|
|
||||||
|
```go
|
||||||
|
api.Use(apiKeyMiddleware)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group(prefix)
|
||||||
|
|
||||||
|
Creates a nested group.
|
||||||
|
|
||||||
|
```go
|
||||||
|
v1 := api.Group("/v1")
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Method Shortcuts
|
||||||
|
|
||||||
|
```go
|
||||||
|
api.Get("/users", listUsersHandler)
|
||||||
|
api.Post("/users", createUserHandler)
|
||||||
|
api.Put("/users/[id]", updateUserHandler)
|
||||||
|
api.Patch("/users/[id]", patchUserHandler)
|
||||||
|
api.Delete("/users/[id]", deleteUserHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
### WithMiddleware(mw ...Middleware)
|
||||||
|
|
||||||
|
Applies middleware to the next route registration in this group.
|
||||||
|
|
||||||
|
```go
|
||||||
|
api.WithMiddleware(authMiddleware).Get("/admin", adminHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Parameters
|
||||||
|
|
||||||
|
Dynamic segments in paths are defined using square brackets.
|
||||||
|
|
||||||
|
```go
|
||||||
|
router.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
id := params[0]
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wildcards
|
||||||
|
|
||||||
|
Wildcard segments capture all remaining path segments.
|
||||||
|
|
||||||
|
```go
|
||||||
|
router.Get("/files/*path", func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
path := params[0]
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Wildcards must be the last segment in a path
|
||||||
|
- Only one wildcard is allowed per path
|
275
EXAMPLES.md
Normal file
275
EXAMPLES.md
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
# Router Usage Examples
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"github.com/yourusername/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := router.New()
|
||||||
|
|
||||||
|
r.Get("/", func(w http.ResponseWriter, r *http.Request, _ []string) {
|
||||||
|
fmt.Fprintf(w, "Hello World!")
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Get("/about", func(w http.ResponseWriter, r *http.Request, _ []string) {
|
||||||
|
fmt.Fprintf(w, "About page")
|
||||||
|
})
|
||||||
|
|
||||||
|
http.ListenAndServe(":8080", r)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Parameters
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := router.New()
|
||||||
|
|
||||||
|
// Single parameter
|
||||||
|
r.Get("/users/[id]", func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
id := params[0]
|
||||||
|
fmt.Fprintf(w, "User ID: %s", id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Multiple parameters
|
||||||
|
r.Get("/posts/[category]/[id]", func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
category := params[0]
|
||||||
|
id := params[1]
|
||||||
|
fmt.Fprintf(w, "Category: %s, Post ID: %s", category, id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wildcard
|
||||||
|
r.Get("/files/*path", func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
path := params[0]
|
||||||
|
fmt.Fprintf(w, "File path: %s", path)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Logging middleware
|
||||||
|
func LoggingMiddleware(next router.Handler) router.Handler {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
fmt.Printf("[%s] %s\n", r.Method, r.URL.Path)
|
||||||
|
next(w, r, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth middleware
|
||||||
|
func AuthMiddleware(next router.Handler) router.Handler {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next(w, r, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global middleware
|
||||||
|
r := router.New()
|
||||||
|
r.Use(LoggingMiddleware)
|
||||||
|
|
||||||
|
// Route-specific middleware
|
||||||
|
r.WithMiddleware(AuthMiddleware).Get("/admin", adminHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Groups
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := router.New()
|
||||||
|
|
||||||
|
// API group
|
||||||
|
api := r.Group("/api")
|
||||||
|
api.Get("/status", statusHandler)
|
||||||
|
|
||||||
|
// Versioned API
|
||||||
|
v1 := api.Group("/v1")
|
||||||
|
v1.Get("/users", listUsersHandler)
|
||||||
|
v1.Post("/users", createUserHandler)
|
||||||
|
|
||||||
|
v2 := api.Group("/v2")
|
||||||
|
v2.Get("/users", listUsersV2Handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Combining Features
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := router.New()
|
||||||
|
|
||||||
|
// Global middleware
|
||||||
|
r.Use(LoggingMiddleware)
|
||||||
|
|
||||||
|
// API group with middleware
|
||||||
|
api := r.Group("/api")
|
||||||
|
api.Use(ApiKeyMiddleware)
|
||||||
|
|
||||||
|
// Admin group with auth middleware
|
||||||
|
admin := r.Group("/admin")
|
||||||
|
admin.Use(AuthMiddleware)
|
||||||
|
|
||||||
|
// Users endpoints with versioning
|
||||||
|
users := api.Group("/v1/users")
|
||||||
|
users.Get("/", listUsersHandler)
|
||||||
|
users.Post("/", createUserHandler)
|
||||||
|
users.Get("/[id]", getUserHandler)
|
||||||
|
users.Put("/[id]", updateUserHandler)
|
||||||
|
users.Delete("/[id]", deleteUserHandler)
|
||||||
|
|
||||||
|
// Special case with route-specific middleware
|
||||||
|
api.WithMiddleware(CacheMiddleware).Get("/cached-resource", cachedResourceHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```go
|
||||||
|
r := router.New()
|
||||||
|
|
||||||
|
err := r.Get("/users/[id]", getUserHandler)
|
||||||
|
if err != nil {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom NotFound handler
|
||||||
|
r.ServeHTTP = func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
h, params, ok := r.Lookup(req.Method, req.URL.Path)
|
||||||
|
if !ok {
|
||||||
|
// Custom 404 handler
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, "Custom 404: %s not found", req.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h(w, req, params)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Application Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"github.com/yourusername/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := router.New()
|
||||||
|
|
||||||
|
// Global middleware
|
||||||
|
r.Use(LoggingMiddleware)
|
||||||
|
|
||||||
|
// Basic routes
|
||||||
|
r.Get("/", homeHandler)
|
||||||
|
r.Get("/about", aboutHandler)
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
api := r.Group("/api")
|
||||||
|
api.Use(ApiKeyMiddleware)
|
||||||
|
|
||||||
|
// Users API
|
||||||
|
users := api.Group("/users")
|
||||||
|
users.Get("/", listUsersHandler)
|
||||||
|
users.Post("/", createUserHandler)
|
||||||
|
users.Get("/[id]", getUserHandler)
|
||||||
|
users.Put("/[id]", updateUserHandler)
|
||||||
|
users.Delete("/[id]", deleteUserHandler)
|
||||||
|
|
||||||
|
// Admin routes with auth
|
||||||
|
admin := r.Group("/admin")
|
||||||
|
admin.Use(AuthMiddleware)
|
||||||
|
admin.Get("/", adminDashboardHandler)
|
||||||
|
admin.Get("/users", adminUsersHandler)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
log.Println("Server starting on :8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
func homeHandler(w http.ResponseWriter, r *http.Request, _ []string) {
|
||||||
|
fmt.Fprintf(w, "Welcome to the home page")
|
||||||
|
}
|
||||||
|
|
||||||
|
func aboutHandler(w http.ResponseWriter, r *http.Request, _ []string) {
|
||||||
|
fmt.Fprintf(w, "About us")
|
||||||
|
}
|
||||||
|
|
||||||
|
func listUsersHandler(w http.ResponseWriter, r *http.Request, _ []string) {
|
||||||
|
fmt.Fprintf(w, "List of users")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserHandler(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
id := params[0]
|
||||||
|
fmt.Fprintf(w, "User details for ID: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUserHandler(w http.ResponseWriter, r *http.Request, _ []string) {
|
||||||
|
// Parse form data or JSON body
|
||||||
|
fmt.Fprintf(w, "User created")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUserHandler(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
id := params[0]
|
||||||
|
fmt.Fprintf(w, "User updated: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteUserHandler(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
id := params[0]
|
||||||
|
fmt.Fprintf(w, "User deleted: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminDashboardHandler(w http.ResponseWriter, r *http.Request, _ []string) {
|
||||||
|
fmt.Fprintf(w, "Admin Dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminUsersHandler(w http.ResponseWriter, r *http.Request, _ []string) {
|
||||||
|
fmt.Fprintf(w, "Admin Users Management")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
func LoggingMiddleware(next router.Handler) router.Handler {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
log.Printf("[%s] %s", r.Method, r.URL.Path)
|
||||||
|
next(w, r, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiKeyMiddleware(next router.Handler) router.Handler {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
apiKey := r.Header.Get("X-API-Key")
|
||||||
|
if apiKey == "" {
|
||||||
|
http.Error(w, "API key required", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next(w, r, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthMiddleware(next router.Handler) router.Handler {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, params []string) {
|
||||||
|
// Check session or JWT
|
||||||
|
authorized := checkUserAuth(r)
|
||||||
|
if !authorized {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next(w, r, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkUserAuth(r *http.Request) bool {
|
||||||
|
// Implementation of auth check
|
||||||
|
return r.Header.Get("Authorization") != ""
|
||||||
|
}
|
||||||
|
```
|
26
README.md
26
README.md
@ -108,29 +108,29 @@ http.ListenAndServe(":8080", r)
|
|||||||
Benchmark comparing Router to the standard `http.ServeMux`:
|
Benchmark comparing Router to the standard `http.ServeMux`:
|
||||||
|
|
||||||
```
|
```
|
||||||
cpu: AMD Ryzen 9 7950X 16-Core Processor
|
cpu: 13th Gen Intel(R) Core(TM) i7-1370P
|
||||||
|
|
||||||
BenchmarkComparison/root_path
|
BenchmarkComparison/root_path
|
||||||
Router: 2.098 ns/op 0 B/op 0 allocs/op
|
Router: 1.798 ns/op 0 B/op 0 allocs/op
|
||||||
ServeMux: 32.010 ns/op 0 B/op 0 allocs/op
|
ServeMux: 40.98 ns/op 0 B/op 0 allocs/op
|
||||||
|
|
||||||
BenchmarkComparison/static_path
|
BenchmarkComparison/static_path
|
||||||
Router: 16.050 ns/op 0 B/op 0 allocs/op
|
Router: 18.41 ns/op 0 B/op 0 allocs/op
|
||||||
ServeMux: 67.980 ns/op 0 B/op 0 allocs/op
|
ServeMux: 86.04 ns/op 0 B/op 0 allocs/op
|
||||||
|
|
||||||
BenchmarkComparison/dynamic_path
|
BenchmarkComparison/dynamic_path
|
||||||
Router: 39.170 ns/op 16 B/op 1 allocs/op
|
Router: 24.23 ns/op 0 B/op 0 allocs/op
|
||||||
ServeMux: 174.000 ns/op 48 B/op 3 allocs/op
|
ServeMux: 221.9 ns/op 48 B/op 3 allocs/op
|
||||||
|
|
||||||
BenchmarkComparison/not_found
|
BenchmarkComparison/not_found
|
||||||
Router: 10.580 ns/op 0 B/op 0 allocs/op
|
Router: 10.76 ns/op 0 B/op 0 allocs/op
|
||||||
ServeMux: 178.100 ns/op 56 B/op 3 allocs/op
|
ServeMux: 210.2 ns/op 56 B/op 3 allocs/op
|
||||||
```
|
```
|
||||||
|
|
||||||
- Root path lookups are 15x faster
|
- Root path lookups are 22x faster
|
||||||
- Static paths are 4x faster with zero allocations
|
- Static paths are 4.7x faster with zero allocations
|
||||||
- Dynamic paths are 4.4x faster with fewer allocations
|
- Dynamic paths are 9x faster with zero allocations
|
||||||
- Not found paths are 16.8x faster with zero allocations
|
- Not found paths are 19.5x faster with zero allocations
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
38
router.go
38
router.go
@ -30,6 +30,7 @@ type node struct {
|
|||||||
type Router struct {
|
type Router struct {
|
||||||
get, post, put, patch, delete *node
|
get, post, put, patch, delete *node
|
||||||
middleware []Middleware
|
middleware []Middleware
|
||||||
|
paramsBuffer []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
@ -47,6 +48,7 @@ func New() *Router {
|
|||||||
patch: &node{},
|
patch: &node{},
|
||||||
delete: &node{},
|
delete: &node{},
|
||||||
middleware: []Middleware{},
|
middleware: []Middleware{},
|
||||||
|
paramsBuffer: make([]string, 64),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,18 +354,28 @@ func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
|
|||||||
return nil, nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
return root.handler, []string{}, root.handler != nil
|
return root.handler, nil, root.handler != nil
|
||||||
}
|
}
|
||||||
params := make([]string, 0, root.maxParams)
|
|
||||||
h, found := match(root, path, 0, ¶ms)
|
buffer := r.paramsBuffer
|
||||||
|
if cap(buffer) < int(root.maxParams) {
|
||||||
|
buffer = make([]string, root.maxParams)
|
||||||
|
r.paramsBuffer = buffer
|
||||||
|
}
|
||||||
|
buffer = buffer[:0]
|
||||||
|
|
||||||
|
h, paramCount, found := match(root, path, 0, &buffer)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, nil, false
|
return nil, nil, false
|
||||||
}
|
}
|
||||||
return h, params, true
|
|
||||||
|
return h, buffer[:paramCount], true
|
||||||
}
|
}
|
||||||
|
|
||||||
// match traverses the trie to find a handler.
|
// match traverses the trie to find a handler.
|
||||||
func match(current *node, path string, start int, params *[]string) (Handler, bool) {
|
func match(current *node, path string, start int, params *[]string) (Handler, int, bool) {
|
||||||
|
paramCount := 0
|
||||||
|
|
||||||
for _, c := range current.children {
|
for _, c := range current.children {
|
||||||
if c.isWildcard {
|
if c.isWildcard {
|
||||||
rem := path[start:]
|
rem := path[start:]
|
||||||
@ -371,26 +383,30 @@ func match(current *node, path string, start int, params *[]string) (Handler, bo
|
|||||||
rem = rem[1:]
|
rem = rem[1:]
|
||||||
}
|
}
|
||||||
*params = append(*params, rem)
|
*params = append(*params, rem)
|
||||||
return c.handler, c.handler != nil
|
return c.handler, 1, c.handler != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seg, pos, more := readSegment(path, start)
|
seg, pos, more := readSegment(path, start)
|
||||||
if seg == "" {
|
if seg == "" {
|
||||||
return current.handler, current.handler != nil
|
return current.handler, 0, current.handler != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range current.children {
|
for _, c := range current.children {
|
||||||
if c.segment == seg || c.isDynamic {
|
if c.segment == seg || c.isDynamic {
|
||||||
if c.isDynamic {
|
if c.isDynamic {
|
||||||
*params = append(*params, seg)
|
*params = append(*params, seg)
|
||||||
|
paramCount++
|
||||||
}
|
}
|
||||||
if !more {
|
if !more {
|
||||||
return c.handler, c.handler != nil
|
return c.handler, paramCount, c.handler != nil
|
||||||
}
|
}
|
||||||
h, ok := match(c, path, pos, params)
|
h, nestedCount, ok := match(c, path, pos, params)
|
||||||
if ok {
|
if ok {
|
||||||
return h, true
|
return h, paramCount + nestedCount, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, false
|
|
||||||
|
return nil, 0, false
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user