From 266de52d320bf5a7f8c040cfee708f4ddbb4ccf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 08:54:31 +0100 Subject: [PATCH 01/10] optimize string manipulation functions and improve error handling --- .golangci.yml | 7 ++- common.go | 32 +++++++++---- convert.go | 120 ++++++++++++++++++++++++++++++++++++++++++------ http.go | 37 +++++++++------ parse.go | 12 ++--- strings.go | 87 ++++++++++++++++++++++++----------- strings_test.go | 6 --- 7 files changed, 221 insertions(+), 80 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4f24327..cd9a776 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -57,9 +57,8 @@ linters: disable-default-exclusions: true exclude-functions: - '(*bytes.Buffer).Write' # always returns nil error - - '(*github.com/valyala/bytebufferpool.ByteBuffer).Write' # always returns nil error - - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteByte' # always returns nil error - - '(*github.com/valyala/bytebufferpool.ByteBuffer).WriteString' # always returns nil error + - '(*strings.Builder).WriteByte' # always returns nil error + - '(*strings.Builder).WriteString' # always returns nil error errchkjson: report-no-exported: true @@ -200,7 +199,7 @@ linters: - name: unchecked-type-assertion disabled: true # TODO: Do not disable - name: unhandled-error - arguments: ['bytes\\.Buffer\\.Write'] + disabled: true - name: enforce-switch-style disabled: true - name: var-naming diff --git a/common.go b/common.go index 8a256c6..902c027 100644 --- a/common.go +++ b/common.go @@ -14,8 +14,6 @@ import ( "reflect" "runtime" "slices" - "strconv" - "strings" "sync" "sync/atomic" @@ -137,29 +135,47 @@ func ConvertToBytes(humanReadableString string) int { return 0 } - var unitPrefixPos, lastNumberPos int - // loop backwards to find the last numeric character and the unit prefix + // Find the last digit position by scanning backwards + // Also identify the unit prefix position in the same pass + lastNumberPos := -1 + unitPrefixPos := 0 for i := strLen - 1; i >= 0; i-- { c := humanReadableString[i] if c >= '0' && c <= '9' { lastNumberPos = i break } - if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' { + // Track the first letter position (unit prefix) from the end + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { unitPrefixPos = i } } + // No digits found + if lastNumberPos < 0 { + return 0 + } + numPart := humanReadableString[:lastNumberPos+1] var size float64 - if strings.IndexByte(numPart, '.') >= 0 { + + // Check for decimal point - use simple byte scan instead of IndexByte + hasDot := false + for i := 0; i <= lastNumberPos; i++ { + if numPart[i] == '.' { + hasDot = true + break + } + } + + if hasDot { var err error - size, err = strconv.ParseFloat(numPart, 64) + size, err = ParseFloat64(numPart) if err != nil { return 0 } } else { - i64, err := strconv.ParseUint(numPart, 10, 64) + i64, err := ParseUint(numPart) if err != nil { return 0 } diff --git a/convert.go b/convert.go index c5f9e0b..41b66fd 100644 --- a/convert.go +++ b/convert.go @@ -10,10 +10,19 @@ import ( "reflect" "strconv" "strings" + "sync" "time" "unsafe" ) +// byteSizePool is a sync.Pool for ByteSize buffer allocation +var byteSizePool = sync.Pool{ + New: func() any { + buf := make([]byte, 0, 16) + return &buf + }, +} + // UnsafeString returns a string pointer without allocation func UnsafeString(b []byte) string { // the new way is slower `return unsafe.String(unsafe.SliceData(b), len(b))` @@ -84,11 +93,21 @@ func ByteSize(bytes uint64) string { return "0B" } - buf := make([]byte, 0, 16) + // Get buffer from pool to reduce allocations + bufPtr, ok := byteSizePool.Get().(*[]byte) + if !ok { + bufPtr = new([]byte) + *bufPtr = make([]byte, 0, 16) + } + buf := (*bufPtr)[:0] + if div == 1 { buf = strconv.AppendUint(buf, bytes, 10) buf = append(buf, unit...) - return UnsafeString(buf) + result := string(buf) // Copy before returning to pool + *bufPtr = buf + byteSizePool.Put(bufPtr) + return result } // Fix: cap bytes to maxSafe for overflow, but format as fractional @@ -106,7 +125,10 @@ func ByteSize(bytes uint64) string { buf = strconv.AppendUint(buf, fractional, 10) } buf = append(buf, unit...) - return UnsafeString(buf) + result := string(buf) // Copy before returning to pool + *bufPtr = buf + byteSizePool.Put(bufPtr) + return result } // ToString Change arg to string @@ -123,15 +145,15 @@ func ToString(arg any, timeFormat ...string) string { case int64: return strconv.FormatInt(v, 10) case uint: - return strconv.Itoa(int(v)) + return strconv.FormatUint(uint64(v), 10) case uint8: - return strconv.FormatInt(int64(v), 10) + return strconv.FormatUint(uint64(v), 10) case uint16: - return strconv.FormatInt(int64(v), 10) + return strconv.FormatUint(uint64(v), 10) case uint32: - return strconv.FormatInt(int64(v), 10) + return strconv.FormatUint(uint64(v), 10) case uint64: - return strconv.FormatInt(int64(v), 10) + return strconv.FormatUint(v, 10) case string: return v case []byte: @@ -151,23 +173,91 @@ func ToString(arg any, timeFormat ...string) string { return ToString(v.Interface(), timeFormat...) case fmt.Stringer: return v.String() + // Handle common pointer types directly to avoid reflection + case *string: + if v != nil { + return *v + } + return "" + case *int: + if v != nil { + return strconv.Itoa(*v) + } + return "0" + case *int64: + if v != nil { + return strconv.FormatInt(*v, 10) + } + return "0" + case *uint64: + if v != nil { + return strconv.FormatUint(*v, 10) + } + return "0" + case *float64: + if v != nil { + return strconv.FormatFloat(*v, 'f', -1, 64) + } + return "0" + case *bool: + if v != nil { + return strconv.FormatBool(*v) + } + return "false" + // Handle common slice types directly to avoid reflection + case []string: + if len(v) == 0 { + return "[]" + } + var buf strings.Builder + buf.Grow(len(v) * 8) // Pre-allocate approximate size + buf.WriteByte('[') + for i, s := range v { + if i > 0 { + buf.WriteByte(' ') + } + buf.WriteString(s) + } + buf.WriteByte(']') + return buf.String() + case []int: + if len(v) == 0 { + return "[]" + } + var buf strings.Builder + buf.Grow(len(v) * 4) // Pre-allocate approximate size + buf.WriteByte('[') + for i, n := range v { + if i > 0 { + buf.WriteByte(' ') + } + buf.WriteString(strconv.Itoa(n)) + } + buf.WriteByte(']') + return buf.String() default: // Check if the type is a pointer by using reflection rv := reflect.ValueOf(arg) - if rv.Kind() == reflect.Ptr && !rv.IsNil() { + kind := rv.Kind() + if kind == reflect.Ptr && !rv.IsNil() { // Dereference the pointer and recursively call ToString return ToString(rv.Elem().Interface(), timeFormat...) - } else if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { + } else if kind == reflect.Slice || kind == reflect.Array { // handle slices + n := rv.Len() + if n == 0 { + return "[]" + } var buf strings.Builder - buf.WriteString("[") //nolint:errcheck // no need to check error - for i := 0; i < rv.Len(); i++ { + buf.Grow(n * 8) // Pre-allocate approximate size + buf.WriteByte('[') + for i := 0; i < n; i++ { if i > 0 { - buf.WriteString(" ") //nolint:errcheck // no need to check error + buf.WriteByte(' ') } - buf.WriteString(ToString(rv.Index(i).Interface())) //nolint:errcheck // no need to check error + buf.WriteString(ToString(rv.Index(i).Interface())) } - buf.WriteString("]") //nolint:errcheck // no need to check error + buf.WriteByte(']') return buf.String() } diff --git a/http.go b/http.go index 37f1d63..2caa0a8 100644 --- a/http.go +++ b/http.go @@ -16,25 +16,29 @@ func GetMIME(extension string) string { if len(extension) == 0 { return "" } - var foundMime string + + // Normalize extension once at the start to avoid repeated checks + var extWithoutDot string + var extWithDot string if extension[0] == '.' { - foundMime = mimeExtensions[extension[1:]] + extWithoutDot = extension[1:] + extWithDot = extension } else { - foundMime = mimeExtensions[extension] + extWithoutDot = extension + extWithDot = "." + extension } - if len(foundMime) == 0 { - if extension[0] != '.' { - foundMime = mime.TypeByExtension("." + extension) - } else { - foundMime = mime.TypeByExtension(extension) - } + // Single map lookup with normalized key + if foundMime := mimeExtensions[extWithoutDot]; len(foundMime) > 0 { + return foundMime + } - if foundMime == "" { - return MIMEOctetStream - } + // Fallback to mime package with pre-computed extension + if foundMime := mime.TypeByExtension(extWithDot); foundMime != "" { + return foundMime } - return foundMime + + return MIMEOctetStream } // ParseVendorSpecificContentType check if content type is vendor specific and @@ -70,7 +74,12 @@ func ParseVendorSpecificContentType(cType string, caseInsensitive ...bool) strin return cType } - return working[0:slashIndex+1] + parsableType + // Avoid string concatenation allocation by using pre-allocated buffer + prefixLen := slashIndex + 1 + result := make([]byte, prefixLen+len(parsableType)) + copy(result, working[:prefixLen]) + copy(result[prefixLen:], parsableType) + return UnsafeString(result) } // limits for HTTP statuscodes diff --git a/parse.go b/parse.go index 5a3c96c..51e9c37 100644 --- a/parse.go +++ b/parse.go @@ -79,7 +79,7 @@ func parseDigits[S byteSeq](s S, i int) (uint64, error) { // It supports optional '+' or '-' prefix, checks for overflow and underflow, and returns (0, error) on error. func parseSigned[S byteSeq, T Signed](fn string, s S, minRange, maxRange T) (T, error) { if len(s) == 0 { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} + return 0, &strconv.NumError{Func: fn, Num: "", Err: strconv.ErrSyntax} } neg := false @@ -122,13 +122,11 @@ func parseSigned[S byteSeq, T Signed](fn string, s S, minRange, maxRange T) (T, // It does not support sign prefixes, checks for overflow, and returns (0, error) on error. func parseUnsigned[S byteSeq, T Unsigned](fn string, s S, maxRange T) (T, error) { if len(s) == 0 { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} + return 0, &strconv.NumError{Func: fn, Num: "", Err: strconv.ErrSyntax} } - i := 0 - - // Parse digits - n, err := parseDigits(s, i) + // Parse digits directly from index 0 + n, err := parseDigits(s, 0) // Check for overflow if err != nil { return 0, &strconv.NumError{Func: fn, Num: string(s), Err: err} @@ -144,7 +142,7 @@ func parseUnsigned[S byteSeq, T Unsigned](fn string, s S, maxRange T) (T, error) // on error or overflow. func parseFloat[S byteSeq](fn string, s S) (float64, error) { if len(s) == 0 { - return 0, &strconv.NumError{Func: fn, Num: string(s), Err: strconv.ErrSyntax} + return 0, &strconv.NumError{Func: fn, Num: "", Err: strconv.ErrSyntax} } i := 0 neg := false diff --git a/strings.go b/strings.go index 86cc4e7..c5002a8 100644 --- a/strings.go +++ b/strings.go @@ -6,45 +6,80 @@ package utils // ToLower converts ascii string to lower-case func ToLower(b string) string { - if len(b) == 0 { + n := len(b) + if n == 0 { return b } - for i := 0; i < len(b); i++ { - c := b[i] - low := toLowerTable[c] - if low != c { - res := make([]byte, len(b)) - copy(res, b[:i]) - res[i] = low - for j := i + 1; j < len(b); j++ { - res[j] = toLowerTable[b[j]] - } - return UnsafeString(res) + // Find first character that needs conversion + i := 0 + for ; i < n; i++ { + if toLowerTable[b[i]] != b[i] { + break } } - return b + + // Already lowercase, return original + if i == n { + return b + } + + // Allocate once and copy entire string, then modify in place + res := make([]byte, n) + copy(res, b) + res[i] = toLowerTable[b[i]] + + // Process remaining characters with loop unrolling for better performance + j := i + 1 + for ; j+3 < n; j += 4 { + res[j] = toLowerTable[b[j]] + res[j+1] = toLowerTable[b[j+1]] + res[j+2] = toLowerTable[b[j+2]] + res[j+3] = toLowerTable[b[j+3]] + } + for ; j < n; j++ { + res[j] = toLowerTable[b[j]] + } + + return UnsafeString(res) } // ToUpper converts ascii string to upper-case func ToUpper(b string) string { - if len(b) == 0 { + n := len(b) + if n == 0 { return b } - for i := 0; i < len(b); i++ { - c := b[i] - up := toUpperTable[c] - if up != c { - res := make([]byte, len(b)) - copy(res, b[:i]) - res[i] = up - for j := i + 1; j < len(b); j++ { - res[j] = toUpperTable[b[j]] - } - return UnsafeString(res) + // Find first character that needs conversion + i := 0 + for ; i < n; i++ { + if toUpperTable[b[i]] != b[i] { + break } } - return b + // Already uppercase, return original + if i == n { + return b + } + + // Allocate once and copy entire string, then modify in place + res := make([]byte, n) + copy(res, b) + res[i] = toUpperTable[b[i]] + + // Process remaining characters with loop unrolling for better performance + j := i + 1 + for ; j+3 < n; j += 4 { + res[j] = toUpperTable[b[j]] + res[j+1] = toUpperTable[b[j+1]] + res[j+2] = toUpperTable[b[j+2]] + res[j+3] = toUpperTable[b[j+3]] + } + for ; j < n; j++ { + res[j] = toUpperTable[b[j]] + } + + return UnsafeString(res) } diff --git a/strings_test.go b/strings_test.go index bb60f9c..9557dca 100644 --- a/strings_test.go +++ b/strings_test.go @@ -79,11 +79,8 @@ var benchmarkCases = []TestCase{ } func Test_ToUpper(t *testing.T) { - t.Parallel() for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() result := ToUpper(tc.input) require.Equal(t, tc.upper, result, "ToUpper failed for %s", tc.name) if tc.upperNoConv { @@ -108,11 +105,8 @@ func Test_ToUpper(t *testing.T) { } func Test_ToLower(t *testing.T) { - t.Parallel() for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() result := ToLower(tc.input) require.Equal(t, tc.lower, result, "ToLower failed for %s", tc.name) if tc.lowerNoConv { From 5b6a78307521ae21ee61acf98550665edf5ec219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 09:08:59 +0100 Subject: [PATCH 02/10] optimize loop iteration in convert.go --- convert.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convert.go b/convert.go index 41b66fd..9a58ed8 100644 --- a/convert.go +++ b/convert.go @@ -251,7 +251,7 @@ func ToString(arg any, timeFormat ...string) string { var buf strings.Builder buf.Grow(n * 8) // Pre-allocate approximate size buf.WriteByte('[') - for i := 0; i < n; i++ { + for i := range n { if i > 0 { buf.WriteByte(' ') } From 23e30769f0bb9ae8b219babd0f1888eee0b0de37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 09:13:56 +0100 Subject: [PATCH 03/10] update CI configuration to include Go 1.25.x --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3fd115..4607cdf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: Build: strategy: matrix: - go-version: [1.23.x, 1.24.x] + go-version: [1.23.x, 1.24.x, 1.25.x] platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] runs-on: ${{ matrix.platform }} steps: From c366f6d1e55af83e3bb712c5f9ce9ef058e398e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 09:40:24 +0100 Subject: [PATCH 04/10] optimize integer and boolean formatting functions for improved performance --- convert.go | 38 +++--- format.go | 123 +++++++++++++++++ format_test.go | 357 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 499 insertions(+), 19 deletions(-) create mode 100644 format.go create mode 100644 format_test.go diff --git a/convert.go b/convert.go index 9a58ed8..313d6ae 100644 --- a/convert.go +++ b/convert.go @@ -102,7 +102,7 @@ func ByteSize(bytes uint64) string { buf := (*bufPtr)[:0] if div == 1 { - buf = strconv.AppendUint(buf, bytes, 10) + buf = AppendUint(buf, bytes) buf = append(buf, unit...) result := string(buf) // Copy before returning to pool *bufPtr = buf @@ -119,10 +119,10 @@ func ByteSize(bytes uint64) string { integer := scaled / 10 fractional := scaled % 10 - buf = strconv.AppendUint(buf, integer, 10) + buf = AppendUint(buf, integer) if fractional > 0 { buf = append(buf, '.') - buf = strconv.AppendUint(buf, fractional, 10) + buf = AppendUint(buf, fractional) } buf = append(buf, unit...) result := string(buf) // Copy before returning to pool @@ -135,31 +135,31 @@ func ByteSize(bytes uint64) string { func ToString(arg any, timeFormat ...string) string { switch v := arg.(type) { case int: - return strconv.Itoa(v) + return FormatInt(int64(v)) case int8: - return strconv.FormatInt(int64(v), 10) + return FormatInt8(v) case int16: - return strconv.FormatInt(int64(v), 10) + return FormatInt16(v) case int32: - return strconv.FormatInt(int64(v), 10) + return FormatInt32(v) case int64: - return strconv.FormatInt(v, 10) + return FormatInt(v) case uint: - return strconv.FormatUint(uint64(v), 10) + return FormatUint(uint64(v)) case uint8: - return strconv.FormatUint(uint64(v), 10) + return FormatUint8(v) case uint16: - return strconv.FormatUint(uint64(v), 10) + return FormatUint16(v) case uint32: - return strconv.FormatUint(uint64(v), 10) + return FormatUint32(v) case uint64: - return strconv.FormatUint(v, 10) + return FormatUint(v) case string: return v case []byte: return string(v) case bool: - return strconv.FormatBool(v) + return FormatBool(v) case float32: return strconv.FormatFloat(float64(v), 'f', -1, 32) case float64: @@ -181,17 +181,17 @@ func ToString(arg any, timeFormat ...string) string { return "" case *int: if v != nil { - return strconv.Itoa(*v) + return FormatInt(int64(*v)) } return "0" case *int64: if v != nil { - return strconv.FormatInt(*v, 10) + return FormatInt(*v) } return "0" case *uint64: if v != nil { - return strconv.FormatUint(*v, 10) + return FormatUint(*v) } return "0" case *float64: @@ -201,7 +201,7 @@ func ToString(arg any, timeFormat ...string) string { return "0" case *bool: if v != nil { - return strconv.FormatBool(*v) + return FormatBool(*v) } return "false" // Handle common slice types directly to avoid reflection @@ -231,7 +231,7 @@ func ToString(arg any, timeFormat ...string) string { if i > 0 { buf.WriteByte(' ') } - buf.WriteString(strconv.Itoa(n)) + buf.WriteString(FormatInt(int64(n))) } buf.WriteByte(']') return buf.String() diff --git a/format.go b/format.go new file mode 100644 index 0000000..a1e296d --- /dev/null +++ b/format.go @@ -0,0 +1,123 @@ +package utils + +// smallInts contains precomputed string representations for small integers 0-99 +var smallInts [100]string + +func init() { + for i := range 100 { + smallInts[i] = formatUintSmall(uint64(i)) + } +} + +func formatUintSmall(n uint64) string { + if n < 10 { + return string(byte(n) + '0') + } + return string([]byte{byte(n/10) + '0', byte(n%10) + '0'}) +} + +// formatUintBuf writes the digits of n into buf from the end and returns the start index. +// buf must be at least 20 bytes. +func formatUintBuf(buf *[20]byte, n uint64) int { + i := 20 + for n >= 10 { + i-- + buf[i] = byte(n%10) + '0' + n /= 10 + } + i-- + buf[i] = byte(n) + '0' + return i +} + +// FormatUint formats a uint64 as a decimal string. +// It is faster than strconv.FormatUint for most inputs. +func FormatUint(n uint64) string { + if n < 100 { + return smallInts[n] + } + var buf [20]byte + i := formatUintBuf(&buf, n) + return string(buf[i:]) +} + +// FormatInt formats an int64 as a decimal string. +// It is faster than strconv.FormatInt for most inputs. +func FormatInt(n int64) string { + if n >= 0 { + return FormatUint(uint64(n)) + } + var buf [20]byte + i := formatUintBuf(&buf, uint64(-n)) + i-- + buf[i] = '-' + return string(buf[i:]) +} + +// FormatUint32 formats a uint32 as a decimal string. +func FormatUint32(n uint32) string { + return FormatUint(uint64(n)) +} + +// FormatInt32 formats an int32 as a decimal string. +func FormatInt32(n int32) string { + return FormatInt(int64(n)) +} + +// FormatUint16 formats a uint16 as a decimal string. +func FormatUint16(n uint16) string { + return FormatUint(uint64(n)) +} + +// FormatInt16 formats an int16 as a decimal string. +func FormatInt16(n int16) string { + return FormatInt(int64(n)) +} + +// FormatUint8 formats a uint8 as a decimal string. +func FormatUint8(n uint8) string { + return FormatUint(uint64(n)) +} + +// FormatInt8 formats an int8 as a decimal string. +func FormatInt8(n int8) string { + return FormatInt(int64(n)) +} + +// AppendUint appends the decimal string representation of n to dst. +func AppendUint(dst []byte, n uint64) []byte { + if n < 100 { + return append(dst, smallInts[n]...) + } + var buf [20]byte + i := formatUintBuf(&buf, n) + return append(dst, buf[i:]...) +} + +// AppendInt appends the decimal string representation of n to dst. +func AppendInt(dst []byte, n int64) []byte { + if n >= 0 { + return AppendUint(dst, uint64(n)) + } + var buf [20]byte + i := formatUintBuf(&buf, uint64(-n)) + i-- + buf[i] = '-' + return append(dst, buf[i:]...) +} + +// FormatBool formats a bool as "true" or "false". +func FormatBool(b bool) string { //nolint:revive // we want to keep the syntax consistent + if b { + return "true" + } + return "false" +} + +// AppendBool appends "true" or "false" to dst. +func AppendBool(dst []byte, b bool) []byte { //nolint:revive // we want to keep the syntax consistent + if b { + return append(dst, "true"...) + } + return append(dst, "false"...) +} diff --git a/format_test.go b/format_test.go new file mode 100644 index 0000000..8c71c6f --- /dev/null +++ b/format_test.go @@ -0,0 +1,357 @@ +package utils + +import ( + "math" + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_FormatUint(t *testing.T) { + t.Parallel() + tests := []uint64{ + 0, 1, 9, 10, 11, 99, 100, 101, 999, 1000, + 12345, 123456789, 9999999999, + math.MaxUint32, math.MaxUint64, + } + for _, tt := range tests { + expected := strconv.FormatUint(tt, 10) + result := FormatUint(tt) + require.Equal(t, expected, result, "FormatUint(%d)", tt) + } +} + +func Test_FormatInt(t *testing.T) { + t.Parallel() + tests := []int64{ + 0, 1, -1, 9, -9, 10, -10, 99, -99, 100, -100, + 12345, -12345, 123456789, -123456789, + math.MaxInt32, math.MinInt32, + math.MaxInt64, math.MinInt64, + } + for _, tt := range tests { + expected := strconv.FormatInt(tt, 10) + result := FormatInt(tt) + require.Equal(t, expected, result, "FormatInt(%d)", tt) + } +} + +func Test_FormatUint32(t *testing.T) { + t.Parallel() + tests := []uint32{0, 1, 99, 100, 12345, math.MaxUint32} + for _, tt := range tests { + expected := strconv.FormatUint(uint64(tt), 10) + result := FormatUint32(tt) + require.Equal(t, expected, result, "FormatUint32(%d)", tt) + } +} + +func Test_FormatInt32(t *testing.T) { + t.Parallel() + tests := []int32{0, 1, -1, 99, -99, math.MaxInt32, math.MinInt32} + for _, tt := range tests { + expected := strconv.FormatInt(int64(tt), 10) + result := FormatInt32(tt) + require.Equal(t, expected, result, "FormatInt32(%d)", tt) + } +} + +func Test_FormatUint16(t *testing.T) { + t.Parallel() + tests := []uint16{0, 1, 99, 100, 12345, math.MaxUint16} + for _, tt := range tests { + expected := strconv.FormatUint(uint64(tt), 10) + result := FormatUint16(tt) + require.Equal(t, expected, result, "FormatUint16(%d)", tt) + } +} + +func Test_FormatInt16(t *testing.T) { + t.Parallel() + tests := []int16{0, 1, -1, 99, -99, math.MaxInt16, math.MinInt16} + for _, tt := range tests { + expected := strconv.FormatInt(int64(tt), 10) + result := FormatInt16(tt) + require.Equal(t, expected, result, "FormatInt16(%d)", tt) + } +} + +func Test_FormatUint8(t *testing.T) { + t.Parallel() + tests := []uint8{0, 1, 99, 100, math.MaxUint8} + for _, tt := range tests { + expected := strconv.FormatUint(uint64(tt), 10) + result := FormatUint8(tt) + require.Equal(t, expected, result, "FormatUint8(%d)", tt) + } +} + +func Test_FormatInt8(t *testing.T) { + t.Parallel() + tests := []int8{0, 1, -1, 99, -99, math.MaxInt8, math.MinInt8} + for _, tt := range tests { + expected := strconv.FormatInt(int64(tt), 10) + result := FormatInt8(tt) + require.Equal(t, expected, result, "FormatInt8(%d)", tt) + } +} + +func Test_AppendUint(t *testing.T) { + t.Parallel() + tests := []uint64{0, 1, 99, 100, 12345, 123456789, math.MaxUint64} + for _, tt := range tests { + expected := strconv.AppendUint([]byte("prefix"), tt, 10) + result := AppendUint([]byte("prefix"), tt) + require.Equal(t, expected, result, "AppendUint(%d)", tt) + } +} + +func Test_AppendInt(t *testing.T) { + t.Parallel() + tests := []int64{0, 1, -1, 99, -99, 12345, -12345, math.MaxInt64, math.MinInt64} + for _, tt := range tests { + expected := strconv.AppendInt([]byte("prefix"), tt, 10) + result := AppendInt([]byte("prefix"), tt) + require.Equal(t, expected, result, "AppendInt(%d)", tt) + } +} + +func Test_FormatBool(t *testing.T) { + t.Parallel() + require.Equal(t, "true", FormatBool(true)) + require.Equal(t, "false", FormatBool(false)) +} + +func Test_AppendBool(t *testing.T) { + t.Parallel() + require.Equal(t, []byte("prefixtrue"), AppendBool([]byte("prefix"), true)) + require.Equal(t, []byte("prefixfalse"), AppendBool([]byte("prefix"), false)) +} + +// Benchmarks + +func Benchmark_FormatUint(b *testing.B) { + inputs := []struct { + name string + value uint64 + }{ + {"small", 42}, + {"medium", 123456789}, + {"large", math.MaxUint64}, + } + + for _, input := range inputs { + b.Run(input.name+"/fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatUint(input.value) + } + }) + b.Run(input.name+"/strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatUint(input.value, 10) + } + }) + } +} + +func Benchmark_FormatInt(b *testing.B) { + inputs := []struct { + name string + value int64 + }{ + {"small_pos", 42}, + {"small_neg", -42}, + {"medium_pos", 123456789}, + {"medium_neg", -123456789}, + {"large_pos", math.MaxInt64}, + {"large_neg", math.MinInt64}, + } + + for _, input := range inputs { + b.Run(input.name+"/fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatInt(input.value) + } + }) + b.Run(input.name+"/strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatInt(input.value, 10) + } + }) + } +} + +func Benchmark_FormatUint32(b *testing.B) { + input := uint32(123456789) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatUint32(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatUint(uint64(input), 10) + } + }) +} + +func Benchmark_FormatInt32(b *testing.B) { + input := int32(-123456789) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatInt32(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatInt(int64(input), 10) + } + }) +} + +func Benchmark_FormatUint16(b *testing.B) { + input := uint16(12345) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatUint16(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatUint(uint64(input), 10) + } + }) +} + +func Benchmark_FormatInt16(b *testing.B) { + input := int16(-12345) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatInt16(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatInt(int64(input), 10) + } + }) +} + +func Benchmark_FormatUint8(b *testing.B) { + input := uint8(255) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatUint8(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatUint(uint64(input), 10) + } + }) +} + +func Benchmark_FormatInt8(b *testing.B) { + input := int8(-128) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatInt8(input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatInt(int64(input), 10) + } + }) +} + +func Benchmark_AppendUint(b *testing.B) { + input := uint64(123456789) + dst := make([]byte, 0, 32) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = AppendUint(dst, input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.AppendUint(dst, input, 10) + } + }) +} + +func Benchmark_AppendInt(b *testing.B) { + input := int64(-123456789) + dst := make([]byte, 0, 32) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = AppendInt(dst, input) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.AppendInt(dst, input, 10) + } + }) +} + +func Benchmark_FormatBool(b *testing.B) { + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = FormatBool(true) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.FormatBool(true) + } + }) +} + +func Benchmark_AppendBool(b *testing.B) { + dst := make([]byte, 0, 16) + + b.Run("fiber", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = AppendBool(dst, true) + } + }) + b.Run("strconv", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = strconv.AppendBool(dst, true) + } + }) +} From 95bca37b8804612b39cf74b2da4649c1a8b2bc8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 09:45:12 +0100 Subject: [PATCH 05/10] remove Go 1.25.x from CI configuration matrix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4607cdf..c3fd115 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: Build: strategy: matrix: - go-version: [1.23.x, 1.24.x, 1.25.x] + go-version: [1.23.x, 1.24.x] platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] runs-on: ${{ matrix.platform }} steps: From 6564354b63dbb7ce6d86e7affde97d78bb39847c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 12:14:49 +0100 Subject: [PATCH 06/10] optimize string conversion functions for better performance and reduced allocations --- common.go | 12 ++-------- convert.go | 27 +++------------------ http.go | 7 +----- strings.go | 70 +++++------------------------------------------------- 4 files changed, 12 insertions(+), 104 deletions(-) diff --git a/common.go b/common.go index 902c027..4acc51e 100644 --- a/common.go +++ b/common.go @@ -14,6 +14,7 @@ import ( "reflect" "runtime" "slices" + "strings" "sync" "sync/atomic" @@ -159,16 +160,7 @@ func ConvertToBytes(humanReadableString string) int { numPart := humanReadableString[:lastNumberPos+1] var size float64 - // Check for decimal point - use simple byte scan instead of IndexByte - hasDot := false - for i := 0; i <= lastNumberPos; i++ { - if numPart[i] == '.' { - hasDot = true - break - } - } - - if hasDot { + if strings.IndexByte(numPart, '.') >= 0 { var err error size, err = ParseFloat64(numPart) if err != nil { diff --git a/convert.go b/convert.go index 313d6ae..61adba8 100644 --- a/convert.go +++ b/convert.go @@ -10,19 +10,10 @@ import ( "reflect" "strconv" "strings" - "sync" "time" "unsafe" ) -// byteSizePool is a sync.Pool for ByteSize buffer allocation -var byteSizePool = sync.Pool{ - New: func() any { - buf := make([]byte, 0, 16) - return &buf - }, -} - // UnsafeString returns a string pointer without allocation func UnsafeString(b []byte) string { // the new way is slower `return unsafe.String(unsafe.SliceData(b), len(b))` @@ -93,21 +84,12 @@ func ByteSize(bytes uint64) string { return "0B" } - // Get buffer from pool to reduce allocations - bufPtr, ok := byteSizePool.Get().(*[]byte) - if !ok { - bufPtr = new([]byte) - *bufPtr = make([]byte, 0, 16) - } - buf := (*bufPtr)[:0] + buf := make([]byte, 0, 16) if div == 1 { buf = AppendUint(buf, bytes) buf = append(buf, unit...) - result := string(buf) // Copy before returning to pool - *bufPtr = buf - byteSizePool.Put(bufPtr) - return result + return UnsafeString(buf) } // Fix: cap bytes to maxSafe for overflow, but format as fractional @@ -125,10 +107,7 @@ func ByteSize(bytes uint64) string { buf = AppendUint(buf, fractional) } buf = append(buf, unit...) - result := string(buf) // Copy before returning to pool - *bufPtr = buf - byteSizePool.Put(bufPtr) - return result + return UnsafeString(buf) } // ToString Change arg to string diff --git a/http.go b/http.go index 2caa0a8..db69c5d 100644 --- a/http.go +++ b/http.go @@ -74,12 +74,7 @@ func ParseVendorSpecificContentType(cType string, caseInsensitive ...bool) strin return cType } - // Avoid string concatenation allocation by using pre-allocated buffer - prefixLen := slashIndex + 1 - result := make([]byte, prefixLen+len(parsableType)) - copy(result, working[:prefixLen]) - copy(result[prefixLen:], parsableType) - return UnsafeString(result) + return working[:slashIndex+1] + parsableType } // limits for HTTP statuscodes diff --git a/strings.go b/strings.go index c5002a8..5328d56 100644 --- a/strings.go +++ b/strings.go @@ -6,39 +6,10 @@ package utils // ToLower converts ascii string to lower-case func ToLower(b string) string { - n := len(b) - if n == 0 { - return b - } - - // Find first character that needs conversion - i := 0 - for ; i < n; i++ { - if toLowerTable[b[i]] != b[i] { - break - } - } - - // Already lowercase, return original - if i == n { - return b - } - - // Allocate once and copy entire string, then modify in place - res := make([]byte, n) + res := make([]byte, len(b)) copy(res, b) - res[i] = toLowerTable[b[i]] - - // Process remaining characters with loop unrolling for better performance - j := i + 1 - for ; j+3 < n; j += 4 { - res[j] = toLowerTable[b[j]] - res[j+1] = toLowerTable[b[j+1]] - res[j+2] = toLowerTable[b[j+2]] - res[j+3] = toLowerTable[b[j+3]] - } - for ; j < n; j++ { - res[j] = toLowerTable[b[j]] + for i := range res { + res[i] = toLowerTable[res[i]] } return UnsafeString(res) @@ -46,39 +17,10 @@ func ToLower(b string) string { // ToUpper converts ascii string to upper-case func ToUpper(b string) string { - n := len(b) - if n == 0 { - return b - } - - // Find first character that needs conversion - i := 0 - for ; i < n; i++ { - if toUpperTable[b[i]] != b[i] { - break - } - } - - // Already uppercase, return original - if i == n { - return b - } - - // Allocate once and copy entire string, then modify in place - res := make([]byte, n) + res := make([]byte, len(b)) copy(res, b) - res[i] = toUpperTable[b[i]] - - // Process remaining characters with loop unrolling for better performance - j := i + 1 - for ; j+3 < n; j += 4 { - res[j] = toUpperTable[b[j]] - res[j+1] = toUpperTable[b[j+1]] - res[j+2] = toUpperTable[b[j+2]] - res[j+3] = toUpperTable[b[j+3]] - } - for ; j < n; j++ { - res[j] = toUpperTable[b[j]] + for i := range res { + res[i] = toUpperTable[res[i]] } return UnsafeString(res) From 163a536a62efe5445ecdf0ed8561252a6bf43ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 12:32:03 +0100 Subject: [PATCH 07/10] optimize ToLower and ToUpper functions for improved performance and reduced allocations --- strings.go | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/strings.go b/strings.go index 5328d56..86cc4e7 100644 --- a/strings.go +++ b/strings.go @@ -6,22 +6,45 @@ package utils // ToLower converts ascii string to lower-case func ToLower(b string) string { - res := make([]byte, len(b)) - copy(res, b) - for i := range res { - res[i] = toLowerTable[res[i]] + if len(b) == 0 { + return b } - return UnsafeString(res) + for i := 0; i < len(b); i++ { + c := b[i] + low := toLowerTable[c] + if low != c { + res := make([]byte, len(b)) + copy(res, b[:i]) + res[i] = low + for j := i + 1; j < len(b); j++ { + res[j] = toLowerTable[b[j]] + } + return UnsafeString(res) + } + } + return b } // ToUpper converts ascii string to upper-case func ToUpper(b string) string { - res := make([]byte, len(b)) - copy(res, b) - for i := range res { - res[i] = toUpperTable[res[i]] + if len(b) == 0 { + return b + } + + for i := 0; i < len(b); i++ { + c := b[i] + up := toUpperTable[c] + if up != c { + res := make([]byte, len(b)) + copy(res, b[:i]) + res[i] = up + for j := i + 1; j < len(b); j++ { + res[j] = toUpperTable[b[j]] + } + return UnsafeString(res) + } } - return UnsafeString(res) + return b } From 413b58fc93dc0ae76795a4c7466b2968d0841e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 12:43:50 +0100 Subject: [PATCH 08/10] optimize boolean formatting by replacing custom functions with strconv package for improved performance --- convert.go | 4 ++-- format.go | 16 ---------------- format_test.go | 44 -------------------------------------------- 3 files changed, 2 insertions(+), 62 deletions(-) diff --git a/convert.go b/convert.go index 61adba8..bd4668f 100644 --- a/convert.go +++ b/convert.go @@ -138,7 +138,7 @@ func ToString(arg any, timeFormat ...string) string { case []byte: return string(v) case bool: - return FormatBool(v) + return strconv.FormatBool(v) case float32: return strconv.FormatFloat(float64(v), 'f', -1, 32) case float64: @@ -180,7 +180,7 @@ func ToString(arg any, timeFormat ...string) string { return "0" case *bool: if v != nil { - return FormatBool(*v) + return strconv.FormatBool(*v) } return "false" // Handle common slice types directly to avoid reflection diff --git a/format.go b/format.go index a1e296d..0e777db 100644 --- a/format.go +++ b/format.go @@ -105,19 +105,3 @@ func AppendInt(dst []byte, n int64) []byte { buf[i] = '-' return append(dst, buf[i:]...) } - -// FormatBool formats a bool as "true" or "false". -func FormatBool(b bool) string { //nolint:revive // we want to keep the syntax consistent - if b { - return "true" - } - return "false" -} - -// AppendBool appends "true" or "false" to dst. -func AppendBool(dst []byte, b bool) []byte { //nolint:revive // we want to keep the syntax consistent - if b { - return append(dst, "true"...) - } - return append(dst, "false"...) -} diff --git a/format_test.go b/format_test.go index 8c71c6f..13b51d2 100644 --- a/format_test.go +++ b/format_test.go @@ -117,18 +117,6 @@ func Test_AppendInt(t *testing.T) { } } -func Test_FormatBool(t *testing.T) { - t.Parallel() - require.Equal(t, "true", FormatBool(true)) - require.Equal(t, "false", FormatBool(false)) -} - -func Test_AppendBool(t *testing.T) { - t.Parallel() - require.Equal(t, []byte("prefixtrue"), AppendBool([]byte("prefix"), true)) - require.Equal(t, []byte("prefixfalse"), AppendBool([]byte("prefix"), false)) -} - // Benchmarks func Benchmark_FormatUint(b *testing.B) { @@ -323,35 +311,3 @@ func Benchmark_AppendInt(b *testing.B) { } }) } - -func Benchmark_FormatBool(b *testing.B) { - b.Run("fiber", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = FormatBool(true) - } - }) - b.Run("strconv", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = strconv.FormatBool(true) - } - }) -} - -func Benchmark_AppendBool(b *testing.B) { - dst := make([]byte, 0, 16) - - b.Run("fiber", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = AppendBool(dst, true) - } - }) - b.Run("strconv", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = strconv.AppendBool(dst, true) - } - }) -} From 7f10c3e88f1af2f11313e8b856238b2af643ec6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 12:48:41 +0100 Subject: [PATCH 09/10] optimize benchmark tests for ToUpper and ToLower functions by comparing custom implementations with standard library functions --- strings_test.go | 66 +++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/strings_test.go b/strings_test.go index 9557dca..a4e75ea 100644 --- a/strings_test.go +++ b/strings_test.go @@ -154,11 +154,19 @@ func Benchmark_ToUpper(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(len(tc.input))) b.ResetTimer() - var res string - for n := 0; n < b.N; n++ { - res = ToUpper(tc.input) - } - require.Equal(b, tc.upper, res) + var fiberRes, stdRes string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + fiberRes = ToUpper(tc.input) + } + require.Equal(b, tc.upper, fiberRes) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + stdRes = strings.ToUpper(tc.input) + } + require.Equal(b, tc.upper, stdRes) + }) }) } } @@ -169,41 +177,19 @@ func Benchmark_ToLower(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(len(tc.input))) b.ResetTimer() - var res string - for n := 0; n < b.N; n++ { - res = ToLower(tc.input) - } - require.Equal(b, tc.lower, res) - }) - } -} - -func Benchmark_StdToUpper(b *testing.B) { - for _, tc := range benchmarkCases { - b.Run(tc.name, func(b *testing.B) { - b.ReportAllocs() - b.SetBytes(int64(len(tc.input))) - b.ResetTimer() - var res string - for n := 0; n < b.N; n++ { - res = strings.ToUpper(tc.input) - } - require.Equal(b, tc.upper, res) - }) - } -} - -func Benchmark_StdToLower(b *testing.B) { - for _, tc := range benchmarkCases { - b.Run(tc.name, func(b *testing.B) { - b.ReportAllocs() - b.SetBytes(int64(len(tc.input))) - b.ResetTimer() - var res string - for n := 0; n < b.N; n++ { - res = strings.ToLower(tc.input) - } - require.Equal(b, tc.lower, res) + var fiberRes, stdRes string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + fiberRes = ToLower(tc.input) + } + require.Equal(b, tc.lower, fiberRes) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + stdRes = strings.ToLower(tc.input) + } + require.Equal(b, tc.lower, stdRes) + }) }) } } From 483fcccdef45ae4a49f87c57e0f3967274083d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Fri, 5 Dec 2025 16:25:06 +0100 Subject: [PATCH 10/10] optimize string conversion functions to handle nil pointers and improve performance for small integers --- convert_test.go | 26 ++++++++++++ format.go | 111 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/convert_test.go b/convert_test.go index 122313b..df7e2f9 100644 --- a/convert_test.go +++ b/convert_test.go @@ -164,6 +164,32 @@ func Test_ToString(t *testing.T) { t.Parallel() require.Equal(t, "", ToString(nil)) }) + + // Test nil pointer handling - nil pointers return type-specific defaults + t.Run("nil pointer to string", func(t *testing.T) { + t.Parallel() + require.Empty(t, ToString((*string)(nil))) + }) + t.Run("nil pointer to int", func(t *testing.T) { + t.Parallel() + require.Equal(t, "0", ToString((*int)(nil))) + }) + t.Run("nil pointer to int64", func(t *testing.T) { + t.Parallel() + require.Equal(t, "0", ToString((*int64)(nil))) + }) + t.Run("nil pointer to uint64", func(t *testing.T) { + t.Parallel() + require.Equal(t, "0", ToString((*uint64)(nil))) + }) + t.Run("nil pointer to float64", func(t *testing.T) { + t.Parallel() + require.Equal(t, "0", ToString((*float64)(nil))) + }) + t.Run("nil pointer to bool", func(t *testing.T) { + t.Parallel() + require.Equal(t, "false", ToString((*bool)(nil))) + }) } func TestCopyBytes(t *testing.T) { diff --git a/format.go b/format.go index 0e777db..04d7aaa 100644 --- a/format.go +++ b/format.go @@ -3,9 +3,15 @@ package utils // smallInts contains precomputed string representations for small integers 0-99 var smallInts [100]string +// smallNegInts contains precomputed string representations for small negative integers -1 to -99 +var smallNegInts [100]string + func init() { for i := range 100 { smallInts[i] = formatUintSmall(uint64(i)) + if i > 0 { + smallNegInts[i] = "-" + smallInts[i] + } } } @@ -44,8 +50,16 @@ func FormatUint(n uint64) string { // FormatInt formats an int64 as a decimal string. // It is faster than strconv.FormatInt for most inputs. func FormatInt(n int64) string { + if n >= 0 && n < 100 { + return smallInts[n] + } + if n < 0 && n > -100 { + return smallNegInts[-n] + } if n >= 0 { - return FormatUint(uint64(n)) + var buf [20]byte + i := formatUintBuf(&buf, uint64(n)) + return string(buf[i:]) } var buf [20]byte i := formatUintBuf(&buf, uint64(-n)) @@ -56,32 +70,115 @@ func FormatInt(n int64) string { // FormatUint32 formats a uint32 as a decimal string. func FormatUint32(n uint32) string { - return FormatUint(uint64(n)) + if n < 100 { + return smallInts[n] + } + var buf [10]byte // max 4294967295 + i := 10 + for n >= 10 { + i-- + buf[i] = byte(n%10) + '0' //nolint:gosec // i is always in bounds: starts at 10, decrements max 10 times for uint32 + n /= 10 + } + i-- + buf[i] = byte(n) + '0' //nolint:gosec // i is always >= 0 after loop + return string(buf[i:]) } // FormatInt32 formats an int32 as a decimal string. func FormatInt32(n int32) string { - return FormatInt(int64(n)) + if n >= 0 && n < 100 { + return smallInts[n] + } + if n < 0 && n > -100 { + return smallNegInts[-n] + } + if n >= 0 { + return FormatUint32(uint32(n)) + } + var buf [11]byte // max -2147483648 + un := uint32(-n) + i := 11 + for un >= 10 { + i-- + buf[i] = byte(un%10) + '0' + un /= 10 + } + i-- + buf[i] = byte(un) + '0' + i-- + buf[i] = '-' + return string(buf[i:]) } // FormatUint16 formats a uint16 as a decimal string. func FormatUint16(n uint16) string { - return FormatUint(uint64(n)) + if n < 100 { + return smallInts[n] + } + var buf [5]byte // max 65535 + i := 5 + for n >= 10 { + i-- + buf[i] = byte(n%10) + '0' //nolint:gosec // i is always in bounds: starts at 5, decrements max 5 times for uint16 + n /= 10 + } + i-- + buf[i] = byte(n) + '0' //nolint:gosec // i is always >= 0 after loop + return string(buf[i:]) } // FormatInt16 formats an int16 as a decimal string. func FormatInt16(n int16) string { - return FormatInt(int64(n)) + if n >= 0 && n < 100 { + return smallInts[n] + } + if n < 0 && n > -100 { + return smallNegInts[-n] + } + if n >= 0 { + return FormatUint16(uint16(n)) + } + var buf [6]byte // max -32768 + un := uint16(-n) + i := 6 + for un >= 10 { + i-- + buf[i] = byte(un%10) + '0' //nolint:gosec // i is always in bounds + un /= 10 + } + i-- + buf[i] = byte(un) + '0' //nolint:gosec // i is always >= 1 after loop + i-- + buf[i] = '-' //nolint:gosec // i is always >= 0 after decrement + return string(buf[i:]) } // FormatUint8 formats a uint8 as a decimal string. func FormatUint8(n uint8) string { - return FormatUint(uint64(n)) + if n < 100 { + return smallInts[n] + } + // uint8 max is 255, so max 3 digits + return string([]byte{n/100 + '0', (n/10)%10 + '0', n%10 + '0'}) } // FormatInt8 formats an int8 as a decimal string. func FormatInt8(n int8) string { - return FormatInt(int64(n)) + if n >= 0 && n < 100 { + return smallInts[n] + } + if n < 0 && n > -100 { + return smallNegInts[-n] + } + // Only -128 to -100 and 100 to 127 reach here + if n >= 0 { + un := uint8(n) + return string([]byte{un/100 + '0', (un/10)%10 + '0', un%10 + '0'}) + } + // n is -128 to -100 + un := uint8(-n) + return string([]byte{'-', un/100 + '0', (un/10)%10 + '0', un%10 + '0'}) } // AppendUint appends the decimal string representation of n to dst.