Repository: gin-gonic/gin
Pull request #4252 (2025-05-28 13:58:15)

perf(recover): replace bytes with strings in function for better performance
Below is my local test code.  

recover/original.go
```go
package recover

import (
    "bytes"
    "fmt"
    "os"
    "runtime"
)

var (
    dunno     = []byte("???")
    centerDot = []byte("·")
    dot       = []byte(".")
    slash     = []byte("/")
)

// stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte {
    buf := new(bytes.Buffer) // the returned data
    // As we loop, we open files and read them. These variables record the currently
    // loaded file.
    var lines [][]byte
    var lastFile string
    for i := skip; ; i++ { // Skip the expected number of frames
        pc, file, line, ok := runtime.Caller(i)
        if !ok {
            break
        }
        // Print this much at least.  If we can't find the source, it won't show.
        fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
        if file != lastFile {
            data, err := os.ReadFile(file)
            if err != nil {
                continue
            }
            lines = bytes.Split(data, []byte{'\n'})
            lastFile = file
        }
        fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
    }
    return buf.Bytes()
}

// source returns a space-trimmed slice of the n'th line.
func source(lines [][]byte, n int) []byte {
    n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
    if n < 0 || n >= len(lines) {
        return dunno
    }
    return bytes.TrimSpace(lines[n])
}

// function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) []byte {
    fn := runtime.FuncForPC(pc)
    if fn == nil {
        return dunno
    }
    name := []byte(fn.Name())
    // The name includes the path name to the package, which is unnecessary
    // since the file name is already included.  Plus, it has center dots.
    // That is, we see
    //    runtime/debug.*T·ptrmethod
    // and want
    //    *T.ptrmethod
    // Also the package path might contain dot (e.g. code.google.com/...),
    // so first eliminate the path prefix
    if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
        name = name[lastSlash+1:]
    }
    if period := bytes.Index(name, dot); period >= 0 {
        name = name[period+1:]
    }
    name = bytes.ReplaceAll(name, centerDot, dot)
    return name
}
```

recover/optimized.go
```go
package recover

import (
    "bytes"
    "fmt"
    "os"
    "runtime"
    "strings"
)

const dunno1 = "???"

// stack returns a nicely formatted stack frame, skipping skip frames.
func stack1(skip int) []byte {
    buf := new(bytes.Buffer) // the returned data
    // As we loop, we open files and read them. These variables record the currently
    // loaded file.
    var lines [][]byte
    var lastFile string
    for i := skip; ; i++ { // Skip the expected number of frames
        pc, file, line, ok := runtime.Caller(i)
        if !ok {
            break
        }
        // Print this much at least.  If we can't find the source, it won't show.
        fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
        if file != lastFile {
            data, err := os.ReadFile(file)
            if err != nil {
                continue
            }
            lines = bytes.Split(data, []byte{'\n'})
            lastFile = file
        }
        fmt.Fprintf(buf, "\t%s: %s\n", function1(pc), source(lines, line))
    }
    return buf.Bytes()
}

// function returns, if possible, the name of the function containing the PC.
func function1(pc uintptr) string {
    fn := runtime.FuncForPC(pc)
    if fn == nil {
        return dunno1
    }
    name := fn.Name()
    // The name includes the path name to the package, which is unnecessary
    // since the file name is already included.  Plus, it has center dots.
    // That is, we see
    //    runtime/debug.*T·ptrmethod
    // and want
    //    *T.ptrmethod
    // Also the package path might contain dot (e.g. code.google.com/...),
    // so first eliminate the path prefix
    if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 {
        name = name[lastSlash+1:]
    }
    if period := strings.IndexByte(name, '.'); period >= 0 {
        name = name[period+1:]
    }
    name = strings.ReplaceAll(name, "·", ".")
    return name
}
```

recover/function_name.go
```go
package recover

import (
    "bytes"
    "strings"
)

func fnName(nameStr string) []byte {
    name := []byte(nameStr)
    if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
        name = name[lastSlash+1:]
    }
    if period := bytes.Index(name, dot); period >= 0 {
        name = name[period+1:]
    }
    name = bytes.ReplaceAll(name, centerDot, dot)
    return name
}

func fnName1(name string) string {
    if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 {
        name = name[lastSlash+1:]
    }
    if period := strings.IndexByte(name, '.'); period >= 0 {
        name = name[period+1:]
    }
    name = strings.ReplaceAll(name, "·", ".")
    return name
}
```

recover/stack_test.go
```go
package recover

import (
    "runtime"
    "testing"
)

func BenchmarkFnName(b *testing.B) {
    name := "a·b.c/d"

    b.Run("original", func(b *testing.B) {
        b.ReportAllocs()

        for i := 0; i < b.N; i++ {
            fnName(name)
        }
    })

    b.Run("optimized", func(b *testing.B) {
        b.ReportAllocs()

        for i := 0; i < b.N; i++ {
            fnName1(name)
        }
    })
}

func BenchmarkFunction(b *testing.B) {
    pc, _, _, _ := runtime.Caller(2)

    b.Run("original", func(b *testing.B) {
        b.ReportAllocs()

        for i := 0; i < b.N; i++ {
            _ = function(pc)
        }
    })

    b.Run("optimized", func(b *testing.B) {
        b.ReportAllocs()

        for i := 0; i < b.N; i++ {
            _ = function1(pc)
        }
    })
}

func BenchmarkStack(b *testing.B) {
    b.Run("original", func(b *testing.B) {
        b.ReportAllocs()

        for i := 0; i < b.N; i++ {
            _ = stack(2)
        }
    })

    b.Run("optimized", func(b *testing.B) {
        b.ReportAllocs()

        for i := 0; i < b.N; i++ {
            _ = stack1(2)
        }
    })
}
```

benchmark output
```text
goos: darwin
goarch: arm64
pkg: test_code/test_gin/recover
cpu: Apple M3 Pro
BenchmarkFnName
BenchmarkFnName/original
BenchmarkFnName/original-11         	38319324	        29.97 ns/op	      16 B/op	       2 allocs/op
BenchmarkFnName/optimized
BenchmarkFnName/optimized-11        	160470892	         7.158 ns/op	       0 B/op	       0 allocs/op
BenchmarkFunction
BenchmarkFunction/original
BenchmarkFunction/original-11       	16994358	        71.61 ns/op	      40 B/op	       2 allocs/op
BenchmarkFunction/optimized
BenchmarkFunction/optimized-11      	28404208	        40.54 ns/op	       0 B/op	       0 allocs/op
BenchmarkStack
BenchmarkStack/original
BenchmarkStack/original-11          	   18630	     67586 ns/op	  146613 B/op	      43 allocs/op
BenchmarkStack/optimized
BenchmarkStack/optimized-11         	   17376	     61093 ns/op	  146483 B/op	      37 allocs/op
PASS

Process finished with the exit code 0
```



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/gin-gonic/gin/pull/4252?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.92%. Comparing base [(`3dc1cd6`)](https://app.codecov.io/gh/gin-gonic/gin/commit/3dc1cd6572b4e3a0cd170a15debe546c2c72294f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) to head [(`b8f57f8`)](https://app.codecov.io/gh/gin-gonic/gin/commit/b8f57f8b079014e9e2d44ad663b882bb254c1dcf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).
> Report is 127 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4252      +/-   ##
==========================================
- Coverage   99.21%   98.92%   -0.30%     
==========================================
  Files          42       44       +2     
  Lines        3182     3432     +250     
==========================================
+ Hits         3157     3395     +238     
- Misses         17       26       +9     
- Partials        8       11       +3     
```

| [Flag](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | Coverage Δ | |
|---|---|---|
| [](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [--ldflags="-checklinkname=0" -tags sonic](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.85% <100.00%> (?)` | |
| [-tags go_json](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.85% <100.00%> (?)` | |
| [-tags nomsgpack](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.90% <100.00%> (?)` | |
| [go-1.18](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.19](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.20](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.21](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `?` | |
| [go-1.23](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.92% <100.00%> (?)` | |
| [go-1.24](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.92% <100.00%> (?)` | |
| [macos-latest](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.92% <100.00%> (-0.30%)` | :arrow_down: |
| [ubuntu-latest](https://app.codecov.io/gh/gin-gonic/gin/pull/4252/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic) | `98.92% <100.00%> (-0.30%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/gin-gonic/gin/pull/4252?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gin-gonic).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
appleboy: @1911860538 Can you provide the HTTP benchmark report using https://github.com/gin-gonic/go-web-framework-benchmark? Show the before and after results. Thanks. 

Patch:
diff --git a/recovery.go b/recovery.go
index 8e4633b..b3543e4 100644
--- a/recovery.go
+++ b/recovery.go
@@ -19,12 +19,9 @@ import (
 	"time"
 )
 
-var (
-	dunno     = []byte("???")
-	centerDot = []byte("·")
-	dot       = []byte(".")
-	slash     = []byte("/")
-)
+const dunno = "???"
+
+var dunnoBytes = []byte(dunno)
 
 // RecoveryFunc defines the function passable to CustomRecovery.
 type RecoveryFunc func(c *Context, err any)
@@ -138,18 +135,18 @@ func stack(skip int) []byte {
 func source(lines [][]byte, n int) []byte {
 	n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
 	if n < 0 || n >= len(lines) {
-		return dunno
+		return dunnoBytes
 	}
 	return bytes.TrimSpace(lines[n])
 }
 
 // function returns, if possible, the name of the function containing the PC.
-func function(pc uintptr) []byte {
+func function(pc uintptr) string {
 	fn := runtime.FuncForPC(pc)
 	if fn == nil {
 		return dunno
 	}
-	name := []byte(fn.Name())
+	name := fn.Name()
 	// The name includes the path name to the package, which is unnecessary
 	// since the file name is already included.  Plus, it has center dots.
 	// That is, we see
@@ -158,13 +155,13 @@ func function(pc uintptr) []byte {
 	//	*T.ptrmethod
 	// Also the package path might contain dot (e.g. code.google.com/...),
 	// so first eliminate the path prefix
-	if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
+	if lastSlash := strings.LastIndexByte(name, '/'); lastSlash >= 0 {
 		name = name[lastSlash+1:]
 	}
-	if period := bytes.Index(name, dot); period >= 0 {
+	if period := strings.IndexByte(name, '.'); period >= 0 {
 		name = name[period+1:]
 	}
-	name = bytes.ReplaceAll(name, centerDot, dot)
+	name = strings.ReplaceAll(name, "·", ".")
 	return name
 }
 


Test patch:
diff --git a/recovery_test.go b/recovery_test.go
index 08eec1e..a6e61a9 100644
--- a/recovery_test.go
+++ b/recovery_test.go
@@ -90,14 +90,14 @@ func TestPanicWithAbort(t *testing.T) {
 
 func TestSource(t *testing.T) {
 	bs := source(nil, 0)
-	assert.Equal(t, dunno, bs)
+	assert.Equal(t, dunnoBytes, bs)
 
 	in := [][]byte{
 		[]byte("Hello world."),
 		[]byte("Hi, gin.."),
 	}
 	bs = source(in, 10)
-	assert.Equal(t, dunno, bs)
+	assert.Equal(t, dunnoBytes, bs)
 
 	bs = source(in, 1)
 	assert.Equal(t, []byte("Hello world."), bs)