-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
📝 [Proposal]: Always return immutable string and deprecate the Immutable
flag
#3367
Comments
Immutable
flag!Immutable
flag
@ksw2000 Kind of agree with you. The thing i'm not a fan of is having to add so many new functions to be able to return "bytes" |
@gaby Do you have any ideas? I think the first step is to try removing the |
@Fenny What do you think would be against the design concept of being close to the api of expressjs? Also, the promise of zero allocations would no longer apply to string methods? |
I added a new set of benchmarks with ubuntu@ubuntu:~/Desktop/git/fiber$ go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default -benchmem -count=4
goos: linux
goarch: amd64
pkg: github.com/gofiber/fiber/v3
cpu: AMD Ryzen 7 7800X3D 8-Core Processor
Benchmark_Router_Next_Default
Benchmark_Router_Next_Default-4 35451170 31.42 ns/op 0 B/op 0 allocs/op
Benchmark_Router_Next_Default-4 37581970 31.55 ns/op 0 B/op 0 allocs/op
Benchmark_Router_Next_Default-4 37955616 31.86 ns/op 0 B/op 0 allocs/op
Benchmark_Router_Next_Default-4 37459659 31.51 ns/op 0 B/op 0 allocs/op
Benchmark_Router_Next_Default_Parallel
Benchmark_Router_Next_Default_Parallel-4 147950630 8.087 ns/op 0 B/op 0 allocs/op
Benchmark_Router_Next_Default_Parallel-4 141775549 8.234 ns/op 0 B/op 0 allocs/op
Benchmark_Router_Next_Default_Parallel-4 145090087 9.321 ns/op 0 B/op 0 allocs/op
Benchmark_Router_Next_Default_Parallel-4 100000000 10.03 ns/op 0 B/op 0 allocs/op
Benchmark_Router_Next_Default_Immutable
Benchmark_Router_Next_Default_Immutable-4 26837396 40.83 ns/op 3 B/op 1 allocs/op
Benchmark_Router_Next_Default_Immutable-4 28757527 40.87 ns/op 3 B/op 1 allocs/op
Benchmark_Router_Next_Default_Immutable-4 27952845 41.01 ns/op 3 B/op 1 allocs/op
Benchmark_Router_Next_Default_Immutable-4 28038631 41.55 ns/op 3 B/op 1 allocs/op
Benchmark_Router_Next_Default_Parallel_Immutable
Benchmark_Router_Next_Default_Parallel_Immutable-4 100000000 12.11 ns/op 3 B/op 1 allocs/op
Benchmark_Router_Next_Default_Parallel_Immutable-4 106113111 11.52 ns/op 3 B/op 1 allocs/op
Benchmark_Router_Next_Default_Parallel_Immutable-4 100000000 11.26 ns/op 3 B/op 1 allocs/op
Benchmark_Router_Next_Default_Parallel_Immutable-4 100000000 11.32 ns/op 3 B/op 1 allocs/op |
for version 3 we want to stay like it is for now, maybe with the next version we can do it we have to consider this further first, because then the api's are not really the same as the express api's keep the idea open |
What if we use immutable strings always instead of adding new methods with byte suffixes? |
then the performance of the fiber framework would fall sharply and we would violate the basic concept that we are trying to achieve zero allocations |
I have an idea, we can create our type String []byte
// copy the string
func (s String) String() string {
return string(s)
}
// zero-allocation string
func (s String) MutableString() string {
return utils.UnsafeString(s)
}
// zero-allocation
func (s String) Bytes() []byte {
return s
} I think this approach does not require adding extra functions, e.g. func (c *DefaultCtx) Path(override ...string) String {
if len(override) != 0 && string(c.path) != override[0] {
// Set new path to context
c.pathOriginal = override[0]
// Set new path to request context
c.fasthttp.Request.URI().SetPath(c.pathOriginal)
// Prettify path
c.configDependentPaths()
}
// we can directly return c.path, the type of c.path is []byte
// return c.app.getString(c.path) <-- Let user decide to use []byte, string, or unsafe string
return c.path
} Additionally, testing indicates that calling this function does not seem to introduce any extra overhead. func Benchmark_App_Path_Pure(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Path("/foo/bar")
for i := 0; i < b.N; i++ {
_ = c.Path()
}
}
func Benchmark_App_Path_Bytes(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Path("/foo/bar")
for i := 0; i < b.N; i++ {
_ = c.Path().Bytes()
}
}
func Benchmark_App_Path_MutableString(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Path("/foo/bar")
for i := 0; i < b.N; i++ {
_ = c.Path().MutableString()
}
}
func Benchmark_App_Path_String(b *testing.B) {
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Path("/foo/bar")
for i := 0; i < b.N; i++ {
_ = c.Path().String()
}
}
The benchmark shows that wrapping the |
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:
In the above example code, the returned string
s
may be changed after callingfoo
. 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, whilescanner.Text()
returns an immutable string:https://cs.opensource.google/go/go/+/refs/tags/go1.24.1:src/bufio/scan.go;l=105-116;drc=bc2124dab14fa292e18df2937037d782f7868635
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 usefoo
, which returns an immutable string.Additionally, I believe the
Immutable
flag inConfig
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
:We can refactor it as follows:
Related issue: #3236 #3246
Here are some parts that can be refactored
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:
The text was updated successfully, but these errors were encountered: