mirror of
				https://github.com/labstack/echo.git
				synced 2025-10-30 23:57:38 +02:00 
			
		
		
		
	Sub router & group API added, fixes #10
Signed-off-by: Vishal Rana <vr@labstack.com>
This commit is contained in:
		
							
								
								
									
										72
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								README.md
									
									
									
									
									
								
							| @@ -105,13 +105,23 @@ func main() { | ||||
| 	e.Get("/users", getUsers) | ||||
| 	e.Get("/users/:id", getUser) | ||||
|  | ||||
| 	// Sub router | ||||
| 	a := e.Sub("/admin") | ||||
| 	a.Use(func(c *echo.Context) { | ||||
| 		// Security check | ||||
| 	//****************// | ||||
| 	//   Sub router   // | ||||
| 	//****************// | ||||
| 	// Sub - inherits parent middleware | ||||
| 	sub := e.Sub("/sub") | ||||
| 	sub.Use(func(c *echo.Context) { // Middleware | ||||
| 	}) | ||||
| 	a.Get("", func(c *echo.Context) { | ||||
| 		c.String(200, "Welcome to the secured area!") | ||||
| 	sub.Get("/home", func(c *echo.Context) { | ||||
| 		c.String(200, "Sub route /sub/welcome") | ||||
| 	}) | ||||
|  | ||||
| 	// Group - doesn't inherit parent middleware | ||||
| 	grp := e.Group("/group") | ||||
| 	grp.Use(func(c *echo.Context) { // Middleware | ||||
| 	}) | ||||
| 	grp.Get("/home", func(c *echo.Context) { | ||||
| 		c.String(200, "Group route /group/welcome") | ||||
| 	}) | ||||
|  | ||||
| 	// Start server | ||||
| @@ -125,29 +135,29 @@ Based on [julienschmidt/go-http-routing-benchmark] (https://github.com/vishr/go- | ||||
| > Echo: 43700 ns/op, 0 B/op, 0 allocs/op | ||||
|  | ||||
| ``` | ||||
| BenchmarkAce_GithubAll	   		20000	     69126 ns/op	   13792 B/op	     167 allocs/op | ||||
| BenchmarkBear_GithubAll	   		10000	    252699 ns/op	   79952 B/op	     943 allocs/op | ||||
| BenchmarkBeego_GithubAll		 3000	    485692 ns/op	  146272 B/op	    2092 allocs/op | ||||
| BenchmarkEcho_GithubAll	   		30000	     43700 ns/op	       0 B/op	       0 allocs/op | ||||
| BenchmarkBone_GithubAll	    	 1000	   2158467 ns/op	  648016 B/op	    8119 allocs/op | ||||
| BenchmarkDenco_GithubAll   		20000	     83022 ns/op	   20224 B/op	     167 allocs/op | ||||
| BenchmarkGin_GithubAll	   		20000	     72317 ns/op	   13792 B/op	     167 allocs/op | ||||
| BenchmarkGocraftWeb_GithubAll	 5000	    381554 ns/op	  133280 B/op	    1889 allocs/op | ||||
| BenchmarkGoji_GithubAll	    	 3000	    605232 ns/op	   56113 B/op	     334 allocs/op | ||||
| BenchmarkGoJsonRest_GithubAll	 5000	    467810 ns/op	  135995 B/op	    2940 allocs/op | ||||
| BenchmarkGoRestful_GithubAll	  200	   9345441 ns/op	  707604 B/op	    7558 allocs/op | ||||
| BenchmarkGorillaMux_GithubAll	  200	   7043040 ns/op	  153136 B/op	    1791 allocs/op | ||||
| BenchmarkHttpRouter_GithubAll	30000	     52251 ns/op	   13792 B/op	     167 allocs/op | ||||
| BenchmarkHttpTreeMux_GithubAll	10000	    145114 ns/op	   56112 B/op	     334 allocs/op | ||||
| BenchmarkKocha_GithubAll	    10000	    145061 ns/op	   23304 B/op	     843 allocs/op | ||||
| BenchmarkMacaron_GithubAll	     2000	    697957 ns/op	  224960 B/op	    2315 allocs/op | ||||
| BenchmarkMartini_GithubAll	      100	  11651997 ns/op	  237953 B/op	    2686 allocs/op | ||||
| BenchmarkPat_GithubAll	          300	   3951799 ns/op	 1504101 B/op	   32222 allocs/op | ||||
| BenchmarkRevel_GithubAll	     2000	   1129370 ns/op	  345553 B/op	    5918 allocs/op | ||||
| BenchmarkRivet_GithubAll	    10000	    246564 ns/op	   84272 B/op	    1079 allocs/op | ||||
| BenchmarkTango_GithubAll	      500	   3544850 ns/op	 1338664 B/op	   27736 allocs/op | ||||
| BenchmarkTigerTonic_GithubAll	 2000	    979370 ns/op	  241088 B/op	    6052 allocs/op | ||||
| BenchmarkTraffic_GithubAll	      200	   7508743 ns/op	 2664762 B/op	   22390 allocs/op | ||||
| BenchmarkVulcan_GithubAll	     5000	    286727 ns/op	   19894 B/op	     609 allocs/op | ||||
| BenchmarkZeus_GithubAll	         2000	    798335 ns/op	  300688 B/op	    2648 allocs/op | ||||
| BenchmarkAce_GithubAll	   	   20000	     65328 ns/op	   13792 B/op	     167 allocs/op | ||||
| BenchmarkBear_GithubAll	   	   10000	    241852 ns/op	   79952 B/op	     943 allocs/op | ||||
| BenchmarkBeego_GithubAll	    3000	    458234 ns/op	  146272 B/op	    2092 allocs/op | ||||
| BenchmarkBone_GithubAll	    	1000	   1923508 ns/op	  648016 B/op	    8119 allocs/op | ||||
| BenchmarkDenco_GithubAll	   20000	     81294 ns/op	   20224 B/op	     167 allocs/op | ||||
| BenchmarkEcho_GithubAll	   	   30000	     42728 ns/op	       0 B/op	       0 allocs/op | ||||
| BenchmarkGin_GithubAll	   	   20000	     69373 ns/op	   13792 B/op	     167 allocs/op | ||||
| BenchmarkGocraftWeb_GithubAll  10000	    370978 ns/op	  133280 B/op	    1889 allocs/op | ||||
| BenchmarkGoji_GithubAll	    	3000	    542766 ns/op	   56113 B/op	     334 allocs/op | ||||
| BenchmarkGoJsonRest_GithubAll	5000	    452551 ns/op	  135995 B/op	    2940 allocs/op | ||||
| BenchmarkGoRestful_GithubAll	 200	   9500204 ns/op	  707604 B/op	    7558 allocs/op | ||||
| BenchmarkGorillaMux_GithubAll	 200	   6770545 ns/op	  153137 B/op	    1791 allocs/op | ||||
| BenchmarkHttpRouter_GithubAll  30000	     56097 ns/op	   13792 B/op	     167 allocs/op | ||||
| BenchmarkHttpTreeMux_GithubAll 10000	    143175 ns/op	   56112 B/op	     334 allocs/op | ||||
| BenchmarkKocha_GithubAll	   10000	    147959 ns/op	   23304 B/op	     843 allocs/op | ||||
| BenchmarkMacaron_GithubAll	    2000	    724650 ns/op	  224960 B/op	    2315 allocs/op | ||||
| BenchmarkMartini_GithubAll	     100	  10926021 ns/op	  237953 B/op	    2686 allocs/op | ||||
| BenchmarkPat_GithubAll	     	 300	   4525114 ns/op	 1504101 B/op	   32222 allocs/op | ||||
| BenchmarkRevel_GithubAll	    2000	   1172963 ns/op	  345553 B/op	    5918 allocs/op | ||||
| BenchmarkRivet_GithubAll	   10000	    249104 ns/op	   84272 B/op	    1079 allocs/op | ||||
| BenchmarkTango_GithubAll	     300	   4012826 ns/op	 1368581 B/op	   29157 allocs/op | ||||
| BenchmarkTigerTonic_GithubAll	2000	    975450 ns/op	  241088 B/op	    6052 allocs/op | ||||
| BenchmarkTraffic_GithubAll	     200	   7540377 ns/op	 2664762 B/op	   22390 allocs/op | ||||
| BenchmarkVulcan_GithubAll	    5000	    307241 ns/op	   19894 B/op	     609 allocs/op | ||||
| BenchmarkZeus_GithubAll	        2000	    752907 ns/op	  300688 B/op	    2648 allocs/op | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										28
									
								
								echo.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								echo.go
									
									
									
									
									
								
							| @@ -8,7 +8,6 @@ import ( | ||||
|  | ||||
| type ( | ||||
| 	Echo struct { | ||||
| 		id                         uint8 | ||||
| 		Router                     *router | ||||
| 		prefix                     string | ||||
| 		middleware                 []MiddlewareFunc | ||||
| @@ -45,8 +44,6 @@ const ( | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	subs = [128]*Echo{} // Sub routers | ||||
|  | ||||
| 	methods = [...]string{ | ||||
| 		MethodCONNECT, | ||||
| 		MethodDELETE, | ||||
| @@ -80,7 +77,7 @@ func New() (e *Echo) { | ||||
| 			Response: &response{}, | ||||
| 			params:   make(Params, e.maxParam), | ||||
| 			store:    make(store), | ||||
| 			echo:     e, // TODO: Do we need it? | ||||
| 			echo:     e, // TODO: Do we need this? | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| @@ -90,13 +87,20 @@ func New() (e *Echo) { | ||||
| func (h HandlerFunc) ServeHTTP(http.ResponseWriter, *http.Request) { | ||||
| } | ||||
|  | ||||
| // Sub creates a new sub router, inherits all properties from the parent router | ||||
| // including middleware. | ||||
| // Sub creates a new sub router. It inherits all properties from the parent | ||||
| // router, including middleware. | ||||
| func (e *Echo) Sub(pfx string) *Echo { | ||||
| 	s := *e | ||||
| 	s.id++ | ||||
| 	s.prefix = pfx | ||||
| 	subs[s.id] = &s | ||||
| 	return &s | ||||
| } | ||||
|  | ||||
| // Group is simmilar to Sub but excludes inheriting middleware from the parent | ||||
| // router. | ||||
| func (e *Echo) Group(pfx string) *Echo { | ||||
| 	s := *e | ||||
| 	s.prefix = pfx | ||||
| 	s.middleware = nil | ||||
| 	return &s | ||||
| } | ||||
|  | ||||
| @@ -174,7 +178,7 @@ func (e *Echo) Trace(path string, h Handler) { | ||||
| } | ||||
|  | ||||
| func (e *Echo) add(method, path string, h Handler) { | ||||
| 	e.Router.Add(method, e.prefix+path, wrapH(h), e.id) | ||||
| 	e.Router.Add(method, e.prefix+path, wrapH(h), e) | ||||
| } | ||||
|  | ||||
| // Static serves static files. | ||||
| @@ -198,14 +202,10 @@ func (e *Echo) Index(file string) { | ||||
| } | ||||
|  | ||||
| func (e *Echo) ServeHTTP(rw http.ResponseWriter, r *http.Request) { | ||||
| 	h, c, eid := e.Router.Find(r.Method, r.URL.Path) | ||||
| 	h, c, e := e.Router.Find(r.Method, r.URL.Path) | ||||
| 	if h == nil { | ||||
| 		h = e.notFoundHandler | ||||
| 	} | ||||
| 	if eid != 0 { | ||||
| 		// It's a sub router | ||||
| 		e = subs[eid] | ||||
| 	} | ||||
| 	c.reset(rw, r, e) | ||||
| 	// Middleware | ||||
| 	for i := len(e.middleware) - 1; i >= 0; i-- { | ||||
|   | ||||
							
								
								
									
										20
									
								
								echo_test.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								echo_test.go
									
									
									
									
									
								
							| @@ -145,7 +145,7 @@ func TestEchoHandler(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestEchoSub(t *testing.T) { | ||||
| func TestEchoSubGroup(t *testing.T) { | ||||
| 	b := new(bytes.Buffer) | ||||
|  | ||||
| 	e := New() | ||||
| @@ -158,7 +158,13 @@ func TestEchoSub(t *testing.T) { | ||||
| 	s.Use(func(*Context) { | ||||
| 		b.WriteString("2") | ||||
| 	}) | ||||
| 	s.Get("", func(*Context) {}) | ||||
| 	s.Get("/home", func(*Context) {}) | ||||
|  | ||||
| 	g := e.Group("/group") | ||||
| 	g.Use(func(*Context) { | ||||
| 		b.WriteString("3") | ||||
| 	}) | ||||
| 	g.Get("/home", func(*Context) {}) | ||||
|  | ||||
| 	w := httptest.NewRecorder() | ||||
| 	r, _ := http.NewRequest(MethodGET, "/users", nil) | ||||
| @@ -169,11 +175,19 @@ func TestEchoSub(t *testing.T) { | ||||
|  | ||||
| 	b.Reset() | ||||
| 	w = httptest.NewRecorder() | ||||
| 	r, _ = http.NewRequest(MethodGET, "/sub", nil) | ||||
| 	r, _ = http.NewRequest(MethodGET, "/sub/home", nil) | ||||
| 	e.ServeHTTP(w, r) | ||||
| 	if b.String() != "12" { | ||||
| 		t.Errorf("should execute middleware 1 & 2, executed %s", b.String()) | ||||
| 	} | ||||
|  | ||||
| 	b.Reset() | ||||
| 	w = httptest.NewRecorder() | ||||
| 	r, _ = http.NewRequest(MethodGET, "/group/home", nil) | ||||
| 	e.ServeHTTP(w, r) | ||||
| 	if b.String() != "3" { | ||||
| 		t.Errorf("should execute middleware 3, executed %s", b.String()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestEchoMethod(t *testing.T) { | ||||
|   | ||||
| @@ -76,13 +76,23 @@ func main() { | ||||
| 	e.Get("/users", getUsers) | ||||
| 	e.Get("/users/:id", getUser) | ||||
|  | ||||
| 	// Sub router | ||||
| 	a := e.Sub("/admin") | ||||
| 	a.Use(func(c *echo.Context) { | ||||
| 		// Security check | ||||
| 	//****************// | ||||
| 	//   Sub router   // | ||||
| 	//****************// | ||||
| 	// Sub - inherits parent middleware | ||||
| 	sub := e.Sub("/sub") | ||||
| 	sub.Use(func(c *echo.Context) { // Middleware | ||||
| 	}) | ||||
| 	a.Get("", func(c *echo.Context) { | ||||
| 		c.String(200, "Welcome to the secured area!") | ||||
| 	sub.Get("/home", func(c *echo.Context) { | ||||
| 		c.String(200, "Sub route /sub/welcome") | ||||
| 	}) | ||||
|  | ||||
| 	// Group - doesn't inherit parent middleware | ||||
| 	grp := e.Group("/group") | ||||
| 	grp.Use(func(c *echo.Context) { // Middleware | ||||
| 	}) | ||||
| 	grp.Get("/home", func(c *echo.Context) { | ||||
| 		c.String(200, "Group route /group/welcome") | ||||
| 	}) | ||||
|  | ||||
| 	// Start server | ||||
|   | ||||
							
								
								
									
										38
									
								
								router.go
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								router.go
									
									
									
									
									
								
							| @@ -12,7 +12,7 @@ type ( | ||||
| 		prefix  string | ||||
| 		has     ntype // Type of node it contains | ||||
| 		handler HandlerFunc | ||||
| 		eid     uint8 // Echo id | ||||
| 		echo    *Echo | ||||
| 		edges   edges | ||||
| 	} | ||||
| 	edges []*node | ||||
| @@ -44,27 +44,27 @@ func NewRouter(e *Echo) (r *router) { | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (r *router) Add(method, path string, h HandlerFunc, eid uint8) { | ||||
| func (r *router) Add(method, path string, h HandlerFunc, echo *Echo) { | ||||
| 	i := 0 | ||||
| 	l := len(path) | ||||
| 	for ; i < l; i++ { | ||||
| 		if path[i] == ':' { | ||||
| 			r.insert(method, path[:i], nil, eid, pnode) | ||||
| 			r.insert(method, path[:i], nil, echo, pnode) | ||||
| 			for ; i < l && path[i] != '/'; i++ { | ||||
| 			} | ||||
| 			if i == l { | ||||
| 				r.insert(method, path[:i], h, eid, snode) | ||||
| 				r.insert(method, path[:i], h, echo, snode) | ||||
| 				return | ||||
| 			} | ||||
| 			r.insert(method, path[:i], nil, eid, snode) | ||||
| 			r.insert(method, path[:i], nil, echo, snode) | ||||
| 		} else if path[i] == '*' { | ||||
| 			r.insert(method, path[:i], h, eid, anode) | ||||
| 			r.insert(method, path[:i], h, echo, anode) | ||||
| 		} | ||||
| 	} | ||||
| 	r.insert(method, path, h, eid, snode) | ||||
| 	r.insert(method, path, h, echo, snode) | ||||
| } | ||||
|  | ||||
| func (r *router) insert(method, path string, h HandlerFunc, eid uint8, has ntype) { | ||||
| func (r *router) insert(method, path string, h HandlerFunc, echo *Echo, has ntype) { | ||||
| 	cn := r.trees[method] // Current node as root | ||||
| 	search := path | ||||
|  | ||||
| @@ -80,12 +80,12 @@ func (r *router) insert(method, path string, h HandlerFunc, eid uint8, has ntype | ||||
| 			cn.has = has | ||||
| 			if h != nil { | ||||
| 				cn.handler = h | ||||
| 				cn.eid = eid | ||||
| 				cn.echo = echo | ||||
| 			} | ||||
| 			return | ||||
| 		} else if l < pl { | ||||
| 			// Split the node | ||||
| 			n := newNode(cn.prefix[l:], cn.has, cn.handler, cn.eid, cn.edges) | ||||
| 			n := newNode(cn.prefix[l:], cn.has, cn.handler, cn.echo, cn.edges) | ||||
| 			cn.edges = edges{n} // Add to parent | ||||
|  | ||||
| 			// Reset parent node | ||||
| @@ -93,15 +93,15 @@ func (r *router) insert(method, path string, h HandlerFunc, eid uint8, has ntype | ||||
| 			cn.prefix = cn.prefix[:l] | ||||
| 			cn.has = snode | ||||
| 			cn.handler = nil | ||||
| 			cn.eid = 0 | ||||
| 			cn.echo = nil | ||||
|  | ||||
| 			if l == sl { | ||||
| 				// At parent node | ||||
| 				cn.handler = h | ||||
| 				cn.eid = eid | ||||
| 				cn.echo = echo | ||||
| 			} else { | ||||
| 				// Need to fork a node | ||||
| 				n = newNode(search[l:], has, h, eid, edges{}) | ||||
| 				n = newNode(search[l:], has, h, echo, edges{}) | ||||
| 				cn.edges = append(cn.edges, n) | ||||
| 			} | ||||
| 			break | ||||
| @@ -109,7 +109,7 @@ func (r *router) insert(method, path string, h HandlerFunc, eid uint8, has ntype | ||||
| 			search = search[l:] | ||||
| 			e := cn.findEdge(search[0]) | ||||
| 			if e == nil { | ||||
| 				n := newNode(search, has, h, eid, edges{}) | ||||
| 				n := newNode(search, has, h, echo, edges{}) | ||||
| 				cn.edges = append(cn.edges, n) | ||||
| 				break | ||||
| 			} else { | ||||
| @@ -119,20 +119,20 @@ func (r *router) insert(method, path string, h HandlerFunc, eid uint8, has ntype | ||||
| 			// Node already exists | ||||
| 			if h != nil { | ||||
| 				cn.handler = h | ||||
| 				cn.eid = eid | ||||
| 				cn.echo = echo | ||||
| 			} | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newNode(pfx string, has ntype, h HandlerFunc, eid uint8, e edges) (n *node) { | ||||
| func newNode(pfx string, has ntype, h HandlerFunc, echo *Echo, e edges) (n *node) { | ||||
| 	n = &node{ | ||||
| 		label:   pfx[0], | ||||
| 		prefix:  pfx, | ||||
| 		has:     has, | ||||
| 		handler: h, | ||||
| 		eid:     eid, | ||||
| 		echo:    echo, | ||||
| 		edges:   e, | ||||
| 	} | ||||
| 	return | ||||
| @@ -159,7 +159,7 @@ func lcp(a, b string) (i int) { | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (r *router) Find(method, path string) (h HandlerFunc, c *Context, eid uint8) { | ||||
| func (r *router) Find(method, path string) (h HandlerFunc, c *Context, echo *Echo) { | ||||
| 	c = r.echo.pool.Get().(*Context) | ||||
| 	cn := r.trees[method] // Current node as root | ||||
| 	search := path | ||||
| @@ -168,7 +168,7 @@ func (r *router) Find(method, path string) (h HandlerFunc, c *Context, eid uint8 | ||||
| 	for { | ||||
| 		if search == "" || search == cn.prefix { | ||||
| 			h = cn.handler | ||||
| 			eid = cn.eid | ||||
| 			echo = cn.echo | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import ( | ||||
|  | ||||
| func TestRouterStatic(t *testing.T) { | ||||
| 	r := New().Router | ||||
| 	r.Add(MethodGET, "/folders/files/echo.gif", func(c *Context) {}, 0) | ||||
| 	r.Add(MethodGET, "/folders/files/echo.gif", func(c *Context) {}, nil) | ||||
| 	h, _, _ := r.Find(MethodGET, "/folders/files/echo.gif") | ||||
| 	if h == nil { | ||||
| 		t.Fatal("handle not found") | ||||
| @@ -16,7 +16,7 @@ func TestRouterStatic(t *testing.T) { | ||||
|  | ||||
| func TestRouterParam(t *testing.T) { | ||||
| 	r := New().Router | ||||
| 	r.Add(MethodGET, "/users/:id", func(c *Context) {}, 0) | ||||
| 	r.Add(MethodGET, "/users/:id", func(c *Context) {}, nil) | ||||
| 	h, c, _ := r.Find(MethodGET, "/users/1") | ||||
| 	if h == nil { | ||||
| 		t.Fatal("handle not found") | ||||
| @@ -29,7 +29,7 @@ func TestRouterParam(t *testing.T) { | ||||
|  | ||||
| func TestRouterCatchAll(t *testing.T) { | ||||
| 	r := New().Router | ||||
| 	r.Add(MethodGET, "/static/*", func(c *Context) {}, 0) | ||||
| 	r.Add(MethodGET, "/static/*", func(c *Context) {}, nil) | ||||
| 	h, _, _ := r.Find(MethodGET, "/static/*") | ||||
| 	if h == nil { | ||||
| 		t.Fatal("handle not found") | ||||
| @@ -38,7 +38,7 @@ func TestRouterCatchAll(t *testing.T) { | ||||
|  | ||||
| func TestRouterMicroParam(t *testing.T) { | ||||
| 	r := New().Router | ||||
| 	r.Add(MethodGET, "/:a/:b/:c", func(c *Context) {}, 0) | ||||
| 	r.Add(MethodGET, "/:a/:b/:c", func(c *Context) {}, nil) | ||||
| 	h, c, _ := r.Find(MethodGET, "/a/b/c") | ||||
| 	if h == nil { | ||||
| 		t.Fatal("handle not found") | ||||
| @@ -59,7 +59,7 @@ func TestRouterMicroParam(t *testing.T) { | ||||
|  | ||||
| func (n *node) printTree(pfx string, tail bool) { | ||||
| 	p := prefix(tail, pfx, "└── ", "├── ") | ||||
| 	fmt.Printf("%s%s has=%d, h=%v, eid=%d\n", p, n.prefix, n.has, n.handler, n.eid) | ||||
| 	fmt.Printf("%s%s has=%d, h=%v, eid=%d\n", p, n.prefix, n.has, n.handler, n.echo) | ||||
|  | ||||
| 	nodes := n.edges | ||||
| 	l := len(nodes) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user