@@ -19,6 +19,30 @@ import (
1919 "path/filepath"
2020 "strings"
2121 "sync"
22+ "unsafe"
23+ )
24+
25+ // stringToBytes returns a []byte view over s without copying, avoiding the allocation+copy of []byte(s)
26+ // on the response write path (the zero-copy technique used by fasthttp/fiber).
27+ //
28+ // Contract — all of the following must hold at every call site:
29+ // - The result is read-only: writing through it is undefined behaviour.
30+ // - The callee must NOT retain the slice beyond the call. It is only passed to the response
31+ // Writer's Write, whose io.Writer contract forbids retaining/mutating the argument. Note the
32+ // concrete writer may be a wrapping ResponseWriter (e.g. gzip); such writers must copy, not alias.
33+ // - s must stay reachable for as long as the slice is used: the slice aliases s's backing array.
34+ func stringToBytes (s string ) []byte {
35+ if s == "" {
36+ return nil
37+ }
38+ return unsafe .Slice (unsafe .StringData (s ), len (s ))
39+ }
40+
41+ // jsonpOpen and jsonpClose are the constant byte wrappers for JSONP payloads, kept as package-level
42+ // slices to avoid allocating them on every JSONP response.
43+ var (
44+ jsonpOpen = []byte ("(" )
45+ jsonpClose = []byte (");" )
2246)
2347
2448const (
@@ -49,6 +73,14 @@ type Context struct {
4973 route * RouteInfo
5074 pathValues * PathValues
5175
76+ // handler is the route handler resolved during routing. It is invoked by the terminal of the global
77+ // middleware chain (see Echo.buildRouterChains) so that the chain can be compiled once and reused.
78+ handler HandlerFunc
79+
80+ // dsw is reused by json() so that each JSON response does not heap-allocate a delayedStatusWriter.
81+ // It lives on the pooled Context; &c.dsw is a stable, allocation-free pointer.
82+ dsw delayedStatusWriter
83+
5284 store map [string ]any
5385 echo * Echo
5486 logger * slog.Logger
@@ -73,9 +105,9 @@ func NewContext(r *http.Request, w http.ResponseWriter, opts ...any) *Context {
73105}
74106
75107func newContext (r * http.Request , w http.ResponseWriter , e * Echo ) * Context {
108+ // store is created lazily by Set (and reset to nil by Reset), so we deliberately do not allocate a map here.
76109 c := & Context {
77110 pathValues : nil ,
78- store : make (map [string ]any ),
79111 echo : e ,
80112 logger : nil ,
81113 }
@@ -109,10 +141,14 @@ func (c *Context) Reset(r *http.Request, w http.ResponseWriter) {
109141 c .orgResponse .reset (w )
110142 c .response = c .orgResponse
111143 c .query = nil
112- c .store = nil
144+ // clear (rather than nil) keeps the map allocated on the pooled Context so that requests using Set
145+ // do not allocate a fresh map each time. clear(nil) is a no-op.
146+ clear (c .store )
113147 c .logger = c .echo .Logger
114148
115149 c .route = nil
150+ c .handler = nil
151+ c .dsw = delayedStatusWriter {}
116152 c .path = ""
117153 // NOTE: empty by setting length to 0. PathValues has to have capacity of c.echo.contextPathParamAllocSize at all times
118154 * c .pathValues = (* c .pathValues )[:0 ]
@@ -297,10 +333,38 @@ func (c *Context) setPathValues(pv *PathValues) {
297333
298334// QueryParam returns the query param for the provided name.
299335func (c * Context ) QueryParam (name string ) string {
300- if c .query == nil {
301- c .query = c .request .URL .Query ()
336+ // If the full query map was already built (e.g. by QueryParams), use it. Otherwise look the single
337+ // key up directly from the raw query, avoiding the url.Values map allocation for the common case of
338+ // reading only a few params. The result is identical to url.Values.Get on the parsed query.
339+ if c .query != nil {
340+ return c .query .Get (name )
341+ }
342+ return getRawQueryParam (c .request .URL .RawQuery , name )
343+ }
344+
345+ // getRawQueryParam returns the first value for name parsed directly from a raw URL query string. It
346+ // matches url.Values.Get over url.ParseQuery output: first match wins, '+' decodes to space, percent
347+ // escapes are decoded, segments containing ';' are skipped, and pairs whose key or value fail to
348+ // unescape are skipped. It avoids allocating the full url.Values map for single-key lookups.
349+ func getRawQueryParam (query , name string ) string {
350+ for query != "" {
351+ var seg string
352+ seg , query , _ = strings .Cut (query , "&" )
353+ if seg == "" || strings .Contains (seg , ";" ) {
354+ continue
355+ }
356+ key , value , _ := strings .Cut (seg , "=" )
357+ k , err := url .QueryUnescape (key )
358+ if err != nil || k != name {
359+ continue
360+ }
361+ v , err := url .QueryUnescape (value )
362+ if err != nil {
363+ continue
364+ }
365+ return v
302366 }
303- return c . query . Get ( name )
367+ return ""
304368}
305369
306370// QueryParamOr returns the query param or default value for the provided name.
@@ -390,20 +454,21 @@ func (c *Context) Cookies() []*http.Cookie {
390454// Get retrieves data from the context.
391455// Method returns any(nil) when key does not exist which is different from typed nil (eg. []byte(nil)).
392456func (c * Context ) Get (key string ) any {
457+ // Unlock without defer to avoid the deferred-call overhead on this hot path.
393458 c .lock .RLock ()
394- defer c .lock .RUnlock ()
395- return c .store [key ]
459+ v := c .store [key ]
460+ c .lock .RUnlock ()
461+ return v
396462}
397463
398464// Set saves data in the context.
399465func (c * Context ) Set (key string , val any ) {
400466 c .lock .Lock ()
401- defer c .lock .Unlock ()
402-
403467 if c .store == nil {
404468 c .store = make (map [string ]any )
405469 }
406470 c .store [key ] = val
471+ c .lock .Unlock ()
407472}
408473
409474// Bind binds path params, query params and the request body into provided type `i`. The default binder
@@ -445,7 +510,7 @@ func (c *Context) Render(code int, name string, data any) (err error) {
445510
446511// HTML sends an HTTP response with status code.
447512func (c * Context ) HTML (code int , html string ) (err error ) {
448- return c .HTMLBlob (code , [] byte (html ))
513+ return c .HTMLBlob (code , stringToBytes (html ))
449514}
450515
451516// HTMLBlob sends an HTTP blob response with status code.
@@ -455,19 +520,22 @@ func (c *Context) HTMLBlob(code int, b []byte) (err error) {
455520
456521// String sends a string response with status code.
457522func (c * Context ) String (code int , s string ) (err error ) {
458- return c .Blob (code , MIMETextPlainCharsetUTF8 , [] byte (s ))
523+ return c .Blob (code , MIMETextPlainCharsetUTF8 , stringToBytes (s ))
459524}
460525
461526func (c * Context ) jsonPBlob (code int , callback string , i any ) (err error ) {
462527 c .writeContentType (MIMEApplicationJavaScriptCharsetUTF8 )
463528 c .response .WriteHeader (code )
464- if _ , err = c .response .Write ([]byte (callback + "(" )); err != nil {
529+ if _ , err = c .response .Write (stringToBytes (callback )); err != nil {
530+ return
531+ }
532+ if _ , err = c .response .Write (jsonpOpen ); err != nil {
465533 return
466534 }
467535 if err = c .echo .JSONSerializer .Serialize (c , i , "" ); err != nil {
468536 return
469537 }
470- if _ , err = c .response .Write ([] byte ( ");" ) ); err != nil {
538+ if _ , err = c .response .Write (jsonpClose ); err != nil {
471539 return
472540 }
473541 return
@@ -480,7 +548,16 @@ func (c *Context) json(code int, i any, indent string) error {
480548 // (global) error handler decides correct status code for the error to be sent to the client.
481549 // For that we need to use writer that can store the proposed status code until the first Write is called.
482550 resp := c .Response ()
483- c .SetResponse (& delayedStatusWriter {ResponseWriter : resp , status : code })
551+ // Reuse the Context-owned delayedStatusWriter to avoid heap-allocating one per JSON response.
552+ // If we are already nested inside a delayed write (rare: a serializer or handler calling c.JSON
553+ // re-entrantly), allocate a fresh writer so the outer call's writer (which is &c.dsw) is not
554+ // clobbered — reusing c.dsw here would make it reference itself.
555+ if _ , nested := resp .(* delayedStatusWriter ); nested {
556+ c .SetResponse (& delayedStatusWriter {ResponseWriter : resp , status : code })
557+ } else {
558+ c .dsw = delayedStatusWriter {ResponseWriter : resp , status : code }
559+ c .SetResponse (& c .dsw )
560+ }
484561 defer c .SetResponse (resp )
485562
486563 return c .echo .JSONSerializer .Serialize (c , i , indent )
@@ -512,13 +589,16 @@ func (c *Context) JSONP(code int, callback string, i any) (err error) {
512589func (c * Context ) JSONPBlob (code int , callback string , b []byte ) (err error ) {
513590 c .writeContentType (MIMEApplicationJavaScriptCharsetUTF8 )
514591 c .response .WriteHeader (code )
515- if _ , err = c .response .Write ([]byte (callback + "(" )); err != nil {
592+ if _ , err = c .response .Write (stringToBytes (callback )); err != nil {
593+ return
594+ }
595+ if _ , err = c .response .Write (jsonpOpen ); err != nil {
516596 return
517597 }
518598 if _ , err = c .response .Write (b ); err != nil {
519599 return
520600 }
521- _ , err = c .response .Write ([] byte ( ");" ) )
601+ _ , err = c .response .Write (jsonpClose )
522602 return
523603}
524604
0 commit comments