@@ -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,16 @@ 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. Only json() may point
82+ // the response at &c.dsw, and only via the nested-call guard there — aliasing it to itself (wrapping
83+ // &c.dsw around &c.dsw) would make the response writer reference itself.
84+ dsw delayedStatusWriter
85+
5286 store map [string ]any
5387 echo * Echo
5488 logger * slog.Logger
@@ -73,9 +107,9 @@ func NewContext(r *http.Request, w http.ResponseWriter, opts ...any) *Context {
73107}
74108
75109func newContext (r * http.Request , w http.ResponseWriter , e * Echo ) * Context {
110+ // store is created lazily by Set and cleared (not freed) by Reset, so we deliberately do not allocate a map here.
76111 c := & Context {
77112 pathValues : nil ,
78- store : make (map [string ]any ),
79113 echo : e ,
80114 logger : nil ,
81115 }
@@ -109,10 +143,14 @@ func (c *Context) Reset(r *http.Request, w http.ResponseWriter) {
109143 c .orgResponse .reset (w )
110144 c .response = c .orgResponse
111145 c .query = nil
112- c .store = nil
146+ // clear (rather than nil) keeps the map allocated on the pooled Context so that requests using Set
147+ // do not allocate a fresh map each time. clear(nil) is a no-op.
148+ clear (c .store )
113149 c .logger = c .echo .Logger
114150
115151 c .route = nil
152+ c .handler = nil
153+ c .dsw = delayedStatusWriter {}
116154 c .path = ""
117155 // NOTE: empty by setting length to 0. PathValues has to have capacity of c.echo.contextPathParamAllocSize at all times
118156 * c .pathValues = (* c .pathValues )[:0 ]
@@ -297,10 +335,38 @@ func (c *Context) setPathValues(pv *PathValues) {
297335
298336// QueryParam returns the query param for the provided name.
299337func (c * Context ) QueryParam (name string ) string {
300- if c .query == nil {
301- c .query = c .request .URL .Query ()
338+ // If the full query map was already built (e.g. by QueryParams), use it. Otherwise look the single
339+ // key up directly from the raw query, avoiding the url.Values map allocation for the common case of
340+ // reading only a few params. The result is identical to url.Values.Get on the parsed query.
341+ if c .query != nil {
342+ return c .query .Get (name )
343+ }
344+ return getRawQueryParam (c .request .URL .RawQuery , name )
345+ }
346+
347+ // getRawQueryParam returns the first value for name parsed directly from a raw URL query string. It
348+ // matches url.Values.Get over url.ParseQuery output: first match wins, '+' decodes to space, percent
349+ // escapes are decoded, segments containing ';' are skipped, and pairs whose key or value fail to
350+ // unescape are skipped. It avoids allocating the full url.Values map for single-key lookups.
351+ func getRawQueryParam (query , name string ) string {
352+ for query != "" {
353+ var seg string
354+ seg , query , _ = strings .Cut (query , "&" )
355+ if seg == "" || strings .Contains (seg , ";" ) {
356+ continue
357+ }
358+ key , value , _ := strings .Cut (seg , "=" )
359+ k , err := url .QueryUnescape (key )
360+ if err != nil || k != name {
361+ continue
362+ }
363+ v , err := url .QueryUnescape (value )
364+ if err != nil {
365+ continue
366+ }
367+ return v
302368 }
303- return c . query . Get ( name )
369+ return ""
304370}
305371
306372// QueryParamOr returns the query param or default value for the provided name.
@@ -390,20 +456,21 @@ func (c *Context) Cookies() []*http.Cookie {
390456// Get retrieves data from the context.
391457// Method returns any(nil) when key does not exist which is different from typed nil (eg. []byte(nil)).
392458func (c * Context ) Get (key string ) any {
459+ // Unlock without defer to avoid the deferred-call overhead on this hot path.
393460 c .lock .RLock ()
394- defer c .lock .RUnlock ()
395- return c .store [key ]
461+ v := c .store [key ]
462+ c .lock .RUnlock ()
463+ return v
396464}
397465
398466// Set saves data in the context.
399467func (c * Context ) Set (key string , val any ) {
400468 c .lock .Lock ()
401- defer c .lock .Unlock ()
402-
403469 if c .store == nil {
404470 c .store = make (map [string ]any )
405471 }
406472 c .store [key ] = val
473+ c .lock .Unlock ()
407474}
408475
409476// Bind binds path params, query params and the request body into provided type `i`. The default binder
@@ -445,7 +512,7 @@ func (c *Context) Render(code int, name string, data any) (err error) {
445512
446513// HTML sends an HTTP response with status code.
447514func (c * Context ) HTML (code int , html string ) (err error ) {
448- return c .HTMLBlob (code , [] byte (html ))
515+ return c .HTMLBlob (code , stringToBytes (html ))
449516}
450517
451518// HTMLBlob sends an HTTP blob response with status code.
@@ -455,19 +522,22 @@ func (c *Context) HTMLBlob(code int, b []byte) (err error) {
455522
456523// String sends a string response with status code.
457524func (c * Context ) String (code int , s string ) (err error ) {
458- return c .Blob (code , MIMETextPlainCharsetUTF8 , [] byte (s ))
525+ return c .Blob (code , MIMETextPlainCharsetUTF8 , stringToBytes (s ))
459526}
460527
461528func (c * Context ) jsonPBlob (code int , callback string , i any ) (err error ) {
462529 c .writeContentType (MIMEApplicationJavaScriptCharsetUTF8 )
463530 c .response .WriteHeader (code )
464- if _ , err = c .response .Write ([]byte (callback + "(" )); err != nil {
531+ if _ , err = c .response .Write (stringToBytes (callback )); err != nil {
532+ return
533+ }
534+ if _ , err = c .response .Write (jsonpOpen ); err != nil {
465535 return
466536 }
467537 if err = c .echo .JSONSerializer .Serialize (c , i , "" ); err != nil {
468538 return
469539 }
470- if _ , err = c .response .Write ([] byte ( ");" ) ); err != nil {
540+ if _ , err = c .response .Write (jsonpClose ); err != nil {
471541 return
472542 }
473543 return
@@ -480,7 +550,16 @@ func (c *Context) json(code int, i any, indent string) error {
480550 // (global) error handler decides correct status code for the error to be sent to the client.
481551 // For that we need to use writer that can store the proposed status code until the first Write is called.
482552 resp := c .Response ()
483- c .SetResponse (& delayedStatusWriter {ResponseWriter : resp , status : code })
553+ // Reuse the Context-owned delayedStatusWriter to avoid heap-allocating one per JSON response.
554+ // If we are already nested inside a delayed write (rare: a serializer or handler calling c.JSON
555+ // re-entrantly), allocate a fresh writer so the outer call's writer (which is &c.dsw) is not
556+ // clobbered — reusing c.dsw here would make it reference itself.
557+ if _ , nested := resp .(* delayedStatusWriter ); nested {
558+ c .SetResponse (& delayedStatusWriter {ResponseWriter : resp , status : code })
559+ } else {
560+ c .dsw = delayedStatusWriter {ResponseWriter : resp , status : code }
561+ c .SetResponse (& c .dsw )
562+ }
484563 defer c .SetResponse (resp )
485564
486565 return c .echo .JSONSerializer .Serialize (c , i , indent )
@@ -512,13 +591,16 @@ func (c *Context) JSONP(code int, callback string, i any) (err error) {
512591func (c * Context ) JSONPBlob (code int , callback string , b []byte ) (err error ) {
513592 c .writeContentType (MIMEApplicationJavaScriptCharsetUTF8 )
514593 c .response .WriteHeader (code )
515- if _ , err = c .response .Write ([]byte (callback + "(" )); err != nil {
594+ if _ , err = c .response .Write (stringToBytes (callback )); err != nil {
595+ return
596+ }
597+ if _ , err = c .response .Write (jsonpOpen ); err != nil {
516598 return
517599 }
518600 if _ , err = c .response .Write (b ); err != nil {
519601 return
520602 }
521- _ , err = c .response .Write ([] byte ( ");" ) )
603+ _ , err = c .response .Write (jsonpClose )
522604 return
523605}
524606
0 commit comments