Description
Feature Proposal Description
In Golang, using mutable strings is not intuitive. Some code in Fiber modifies strings. In the current implementation, we use the Immutable
flag to control whether the strings returned by certain functions are immutable.
Consider the following example:
func main() {
o := newObj()
s := o.GetB()
fmt.Println(s)
o.foo()
fmt.Println(s)
}
type obj struct {
b []byte
}
func newObj() obj {
return obj{
b: []byte("hey!"),
}
}
func (o *obj) GetB() string {
return utils.UnsafeString(o.b)
}
func (o *obj) foo() {
for i := 0; i < len(o.b)/2; i++ {
o.b[i], o.b[len(o.b)-i-1] = o.b[len(o.b)-i-1], o.b[i]
}
}
> go run main.go
hey!
!yeh
In the above example code, the returned string s
may be changed after calling foo
. This is not intuitive in Golang!
The overhead of utils.UnsafeString
is lower than directly copying a string. However, this approach can cause unnecessary issues.
In the Golang standard library, returned strings are always kept immutable. When performance is a concern, a mutable byte slice is returned instead. For example, in the bufio
package, scanner.Bytes()
returns a mutable byte slice, while scanner.Text()
returns an immutable string:
// Bytes returns the most recent token generated by a call to [Scanner.Scan].
// The underlying array may point to data that will be overwritten
// by a subsequent call to Scan. It does no allocation.
func (s *Scanner) Bytes() []byte {
return s.token
}
// Text returns the most recent token generated by a call to [Scanner.Scan]
// as a newly allocated string holding its bytes.
func (s *Scanner) Text() string {
return string(s.token)
}
Similarly, Fasthttp follows the same principle. Since we wrap Fasthttp, we must adhere to this rule as well. If users are concerned about performance, they can call fooBytes
. Otherwise, they can use foo
, which returns an immutable string.
Additionally, I believe the Immutable
flag in Config
is poorly designed. Users may not fully understand its scope or which functions it affects. Moreover, if a user wants to call function A, which returns an immutable string, while also calling function B to optimize performance without copying, this design does not allow it.
We should follow Golang's standard and Fasthttp’s principles: treating all strings as immutable while providing an alternative function for performance-sensitive cases.
For example, in ctx.go
:
// OriginalURL contains the original request URL.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting to use the value outside the handler.
func (c *DefaultCtx) OriginalURL() string {
return c.app.getString(c.fasthttp.Request.Header.RequestURI())
}
We can refactor it as follows:
// OriginalURL contains the original request URL.
func (c *DefaultCtx) OriginalURL() string {
return string(c.fasthttp.Request.Header.RequestURI())
}
// OriginalURLBytes returns the original request URL as a byte slice.
// The returned value is only valid within the handler. Do not store any references.
func (c *DefaultCtx) OriginalURLBytes() []byte {
return c.fasthttp.Request.Header.RequestURI()
}
Here are some parts that can be refactored
// BodyRaw contains the raw body submitted in a POST request.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
func (c *DefaultCtx) BodyRaw() []byte {
return c.getBody()
}
// CHANGE TO
func (c *DefaultCtx) BodyRaw() string {}
func (c *DefaultCtx) BodyRawBytes() []byte {}
// Cookies are used for getting a cookie value by key.
// Defaults to the empty string "" if the cookie doesn't exist.
// If a default value is given, it will return that value if the cookie doesn't exist.
// The returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting to use the value outside the Handler.
func (c *DefaultCtx) Cookies(key string, defaultValue ...string) string {
return defaultString(c.app.getString(c.fasthttp.Request.Header.Cookie(key)), defaultValue)
}
// CHANGED TO
func (c *DefaultCtx) Cookies(key string, defaultValue ...string) string {}
func (c *DefaultCtx) CookiesBytes(key string, defaultValue ...string) []byte {}
// FormValue returns the first value by key from a MultipartForm.
// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.
// Defaults to the empty string "" if the form value doesn't exist.
// If a default value is given, it will return that value if the form value does not exist.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
func (c *DefaultCtx) FormValue(key string, defaultValue ...string) string {
return defaultString(c.app.getString(c.fasthttp.FormValue(key)), defaultValue)
}
// CHANGE TO
func (c *DefaultCtx) FormValue(key string, defaultValue ...string) string {}
func (c *DefaultCtx) FormValueBytes(key string, defaultValue ...string) []byte {}
func (c *DefaultCtx) Path(override ...string) string {
// ...
return c.app.getString(c.path)
}
// CHANGE TO
func (c *DefaultCtx) Path(override ...string) string {}
func (c *DefaultCtx) PathBytes(override ...string) string []byte {}
There are still a lot of functions can be refactored. I believe the most problematic aspect of Fiber is its use of mutable string, which can easily be misused even in small projects. Additionally, maintaining the Immutable
flag requires a significant amount of maintenance effort. I think this issue needs to be addressed in the Fiber v3.
Checklist:
- I agree to follow Fiber's Code of Conduct.
- I have searched for existing issues that describe my proposal before opening this one.
- I understand that a proposal that does not meet these guidelines may be closed without explanation.