vendor/github.com/olekukonko/cat/fn.go
package cat
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"unsafe"
)
// write writes a value to the given strings.Builder using fast paths to avoid temporary allocations.
// It handles common types like strings, byte slices, integers, floats, and booleans directly for efficiency.
// For other types, it falls back to fmt.Fprint, which may involve allocations.
// This function is optimized for performance in string concatenation scenarios, prioritizing
// common cases like strings and numbers at the top of the type switch for compiler optimization.
// Note: For integers and floats, it uses stack-allocated buffers and strconv.Append* functions to
// convert numbers to strings without heap allocations.
func write(b *strings.Builder, arg any) {
writeValue(b, arg, 0)
}
// writeValue appends the string representation of arg to b, handling recursion with a depth limit.
// It serves as a recursive helper for write, directly handling primitives and delegating complex
// types to writeReflect. The depth parameter prevents excessive recursion in deeply nested structures.
func writeValue(b *strings.Builder, arg any, depth int) {
// Handle recursion depth limit
if depth > maxRecursionDepth {
b.WriteString("...")
return
}
// Handle nil values
if arg == nil {
b.WriteString(nilString)
return
}
// Fast path type switch for all primitive types
switch v := arg.(type) {
case string:
b.WriteString(v)
case []byte:
b.WriteString(bytesToString(v))
case int:
var buf [20]byte
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
case int64:
var buf [20]byte
b.Write(strconv.AppendInt(buf[:0], v, 10))
case int32:
var buf [11]byte
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
case int16:
var buf [6]byte
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
case int8:
var buf [4]byte
b.Write(strconv.AppendInt(buf[:0], int64(v), 10))
case uint:
var buf [20]byte
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
case uint64:
var buf [20]byte
b.Write(strconv.AppendUint(buf[:0], v, 10))
case uint32:
var buf [10]byte
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
case uint16:
var buf [5]byte
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
case uint8:
var buf [3]byte
b.Write(strconv.AppendUint(buf[:0], uint64(v), 10))
case float64:
var buf [24]byte
b.Write(strconv.AppendFloat(buf[:0], v, 'f', -1, 64))
case float32:
var buf [24]byte
b.Write(strconv.AppendFloat(buf[:0], float64(v), 'f', -1, 32))
case bool:
if v {
b.WriteString("true")
} else {
b.WriteString("false")
}
case fmt.Stringer:
b.WriteString(v.String())
case error:
b.WriteString(v.Error())
default:
// Fallback to reflection-based handling
writeReflect(b, arg, depth)
}
}
// writeReflect handles all complex types safely.
func writeReflect(b *strings.Builder, arg any, depth int) {
defer func() {
if r := recover(); r != nil {
b.WriteString("[!reflect panic!]")
}
}()
val := reflect.ValueOf(arg)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
b.WriteString(nilString)
return
}
val = val.Elem()
}
switch val.Kind() {
case reflect.Slice, reflect.Array:
b.WriteByte('[')
for i := 0; i < val.Len(); i++ {
if i > 0 {
b.WriteString(", ") // Use comma-space for readability
}
writeValue(b, val.Index(i).Interface(), depth+1)
}
b.WriteByte(']')
case reflect.Struct:
typ := val.Type()
b.WriteByte('{') // Use {} for structs to follow Go convention
first := true
for i := 0; i < val.NumField(); i++ {
fieldValue := val.Field(i)
if !fieldValue.CanInterface() {
continue // Skip unexported fields
}
if !first {
b.WriteByte(' ') // Use space as separator
}
first = false
b.WriteString(typ.Field(i).Name)
b.WriteByte(':')
writeValue(b, fieldValue.Interface(), depth+1)
}
b.WriteByte('}')
case reflect.Map:
b.WriteByte('{')
keys := val.MapKeys()
sort.Slice(keys, func(i, j int) bool {
// A simple string-based sort for keys
return fmt.Sprint(keys[i].Interface()) < fmt.Sprint(keys[j].Interface())
})
for i, key := range keys {
if i > 0 {
b.WriteByte(' ') // Use space as separator
}
writeValue(b, key.Interface(), depth+1)
b.WriteByte(':')
writeValue(b, val.MapIndex(key).Interface(), depth+1)
}
b.WriteByte('}')
case reflect.Interface:
if val.IsNil() {
b.WriteString(nilString)
return
}
writeValue(b, val.Elem().Interface(), depth+1)
default:
fmt.Fprint(b, arg)
}
}
// valueToString converts any value to a string representation.
// It uses optimized paths for common types to avoid unnecessary allocations.
// For types like integers and floats, it directly uses strconv functions.
// This function is useful for single-argument conversions or as a helper in other parts of the package.
// Unlike write, it returns a string instead of appending to a builder.
func valueToString(arg any) string {
switch v := arg.(type) {
case string:
return v
case []byte:
return bytesToString(v)
case int:
return strconv.Itoa(v)
case int64:
return strconv.FormatInt(v, 10)
case int32:
return strconv.FormatInt(int64(v), 10)
case uint:
return strconv.FormatUint(uint64(v), 10)
case uint64:
return strconv.FormatUint(v, 10)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case bool:
if v {
return "true"
}
return "false"
case fmt.Stringer:
return v.String()
case error:
return v.Error()
default:
return fmt.Sprint(v)
}
}
// estimateWith calculates a conservative estimate of the total string length when concatenating
// the given arguments with a separator. This is used for preallocating capacity in strings.Builder
// to minimize reallocations during building.
// It accounts for the length of separators and estimates the length of each argument based on its type.
// If no arguments are provided, it returns 0.
func estimateWith(sep string, args []any) int {
if len(args) == 0 {
return 0
}
size := len(sep) * (len(args) - 1)
size += estimate(args)
return size
}
// estimate calculates a conservative estimate of the combined string length of the given arguments.
// It iterates over each argument and adds an estimated length based on its type:
// - Strings and byte slices: exact length.
// - Numbers: calculated digit count using numLen or uNumLen.
// - Floats and others: fixed conservative estimates (e.g., 16 or 24 bytes).
// This helper is used internally by estimateWith and focuses solely on the arguments without separators.
func estimate(args []any) int {
var size int
for _, a := range args {
switch v := a.(type) {
case string:
size += len(v)
case []byte:
size += len(v)
case int:
size += numLen(int64(v))
case int8:
size += numLen(int64(v))
case int16:
size += numLen(int64(v))
case int32:
size += numLen(int64(v))
case int64:
size += numLen(v)
case uint:
size += uNumLen(uint64(v))
case uint8:
size += uNumLen(uint64(v))
case uint16:
size += uNumLen(uint64(v))
case uint32:
size += uNumLen(uint64(v))
case uint64:
size += uNumLen(v)
case float32:
size += 16
case float64:
size += 24
case bool:
size += 5 // "false"
case fmt.Stringer, error:
size += 16 // conservative
default:
size += 16 // conservative
}
}
return size
}
// numLen returns the number of characters required to represent the signed integer n as a string.
// It handles negative numbers by adding 1 for the '-' sign and uses a loop to count digits.
// Special handling for math.MinInt64 to avoid overflow when negating.
// Returns 1 for 0, and up to 20 for the largest values.
func numLen(n int64) int {
if n == 0 {
return 1
}
c := 0
if n < 0 {
c = 1 // for '-'
// NOTE: math.MinInt64 negated overflows; handle by adding one digit and returning 20.
if n == -1<<63 {
return 20
}
n = -n
}
for n > 0 {
n /= 10
c++
}
return c
}
// uNumLen returns the number of characters required to represent the unsigned integer n as a string.
// It uses a loop to count digits.
// Returns 1 for 0, and up to 20 for the largest uint64 values.
func uNumLen(n uint64) int {
if n == 0 {
return 1
}
c := 0
for n > 0 {
n /= 10
c++
}
return c
}
// bytesToString converts a byte slice to a string efficiently.
// If the package's UnsafeBytes flag is set (via IsUnsafeBytes()), it uses unsafe operations
// to create a string backed by the same memory as the byte slice, avoiding a copy.
// This is zero-allocation when unsafe is enabled.
// Falls back to standard string(bts) conversion otherwise.
// For empty slices, it returns a constant empty string.
// Compatible with Go 1.20+ unsafe functions like unsafe.String and unsafe.SliceData.
func bytesToString(bts []byte) string {
if len(bts) == 0 {
return empty
}
if IsUnsafeBytes() {
// Go 1.20+: unsafe.String with SliceData (1.20 introduced, 1.22 added SliceData).
return unsafe.String(unsafe.SliceData(bts), len(bts))
}
return string(bts)
}
// recursiveEstimate calculates the estimated string length for potentially nested arguments,
// including the lengths of separators between elements. It recurses on nested []any slices,
// flattening the structure while accounting for separators only between non-empty subparts.
// This function is useful for preallocating capacity in builders for nested concatenation operations.
func recursiveEstimate(sep string, args []any) int {
if len(args) == 0 {
return 0
}
size := 0
needsSep := false
for _, a := range args {
switch v := a.(type) {
case []any:
subSize := recursiveEstimate(sep, v)
if subSize > 0 {
if needsSep {
size += len(sep)
}
size += subSize
needsSep = true
}
default:
if needsSep {
size += len(sep)
}
size += estimate([]any{a})
needsSep = true
}
}
return size
}
// recursiveAdd appends the string representations of potentially nested arguments to the builder.
// It recurses on nested []any slices, effectively flattening the structure by adding leaf values
// directly via b.Add without inserting separators (separators are handled externally if needed).
// This function is designed for efficient concatenation of nested argument lists.
func recursiveAdd(b *Builder, args []any) {
for _, a := range args {
switch v := a.(type) {
case []any:
recursiveAdd(b, v)
default:
b.Add(a)
}
}
}