From e7be27413c7487ff9574526a860aa7b5c31e43f6 Mon Sep 17 00:00:00 2001 From: yusing Date: Thu, 19 Dec 2024 00:54:31 +0800 Subject: [PATCH] small string split join optimization --- internal/docker/container_helper.go | 4 +- internal/docker/label.go | 5 +- internal/entrypoint/entrypoint.go | 3 +- internal/error/nested_error.go | 5 +- internal/logging/logging.go | 5 +- internal/net/http/accesslog/filter.go | 4 +- .../net/http/accesslog/status_code_range.go | 5 +- .../net/http/middleware/cloudflare_real_ip.go | 3 +- internal/net/http/reverse_proxy_mod.go | 3 +- internal/route/types/headers.go | 6 +- internal/route/types/raw_entry.go | 6 +- internal/route/types/stream_port.go | 5 +- internal/route/types/stream_scheme.go | 8 +- internal/utils/serialization.go | 6 +- internal/utils/strutils/parser.go | 4 +- internal/utils/strutils/split_join.go | 81 +++++++++++++++++++ internal/utils/strutils/split_join_test.go | 38 +++++++++ internal/utils/strutils/strconv.go | 12 +-- internal/utils/strutils/string.go | 4 +- internal/watcher/health/monitor/monitor.go | 3 +- 20 files changed, 160 insertions(+), 50 deletions(-) create mode 100644 internal/utils/strutils/split_join.go create mode 100644 internal/utils/strutils/split_join_test.go diff --git a/internal/docker/container_helper.go b/internal/docker/container_helper.go index d3f3a63a..ac1226cf 100644 --- a/internal/docker/container_helper.go +++ b/internal/docker/container_helper.go @@ -33,8 +33,8 @@ func (c containerHelper) getName() string { } func (c containerHelper) getImageName() string { - colonSep := strings.Split(c.Image, ":") - slashSep := strings.Split(colonSep[0], "/") + colonSep := strutils.SplitRune(c.Image, ':') + slashSep := strutils.SplitRune(colonSep[0], '/') return slashSep[len(slashSep)-1] } diff --git a/internal/docker/label.go b/internal/docker/label.go index 188d6a6d..c176e2c9 100644 --- a/internal/docker/label.go +++ b/internal/docker/label.go @@ -1,9 +1,8 @@ package docker import ( - "strings" - E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/utils/strutils" ) type LabelMap = map[string]any @@ -13,7 +12,7 @@ func ParseLabels(labels map[string]string) (LabelMap, E.Error) { errs := E.NewBuilder("labels error") for lbl, value := range labels { - parts := strings.Split(lbl, ".") + parts := strutils.SplitRune(lbl, '.') if parts[0] != NSProxy { continue } diff --git a/internal/entrypoint/entrypoint.go b/internal/entrypoint/entrypoint.go index 452bd651..a16f4549 100644 --- a/internal/entrypoint/entrypoint.go +++ b/internal/entrypoint/entrypoint.go @@ -14,6 +14,7 @@ import ( "github.com/yusing/go-proxy/internal/route/routes" route "github.com/yusing/go-proxy/internal/route/types" "github.com/yusing/go-proxy/internal/task" + "github.com/yusing/go-proxy/internal/utils/strutils" ) var findRouteFunc = findRouteAnyDomain @@ -124,7 +125,7 @@ func Handler(w http.ResponseWriter, r *http.Request) { } func findRouteAnyDomain(host string) (route.HTTPRoute, error) { - hostSplit := strings.Split(host, ".") + hostSplit := strutils.SplitRune(host, '.') n := len(hostSplit) switch { case n == 3: diff --git a/internal/error/nested_error.go b/internal/error/nested_error.go index acff9772..8de28e25 100644 --- a/internal/error/nested_error.go +++ b/internal/error/nested_error.go @@ -3,7 +3,8 @@ package err import ( "errors" "fmt" - "strings" + + "github.com/yusing/go-proxy/internal/utils/strutils" ) //nolint:recvcheck @@ -78,7 +79,7 @@ func (err *nestedError) Error() string { if extras := makeLines(err.Extras, 1); len(extras) > 0 { lines = append(lines, extras...) } - return strings.Join(lines, "\n") + return strutils.JoinLines(lines) } //go:inline diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 69c5136f..dee54dde 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -7,6 +7,7 @@ import ( "github.com/rs/zerolog" "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/utils/strutils" ) var logger zerolog.Logger @@ -39,14 +40,14 @@ func init() { FieldsExclude: exclude, FormatMessage: func(msgI interface{}) string { // pad spaces for each line msg := msgI.(string) - lines := strings.Split(msg, "\n") + lines := strutils.SplitRune(msg, '\n') if len(lines) == 1 { return msg } for i := 1; i < len(lines); i++ { lines[i] = prefix + lines[i] } - return strings.Join(lines, "\n") + return strutils.JoinRune(lines, '\n') }, }, ).Level(level).With().Timestamp().Logger() diff --git a/internal/net/http/accesslog/filter.go b/internal/net/http/accesslog/filter.go index cc1f146d..822101eb 100644 --- a/internal/net/http/accesslog/filter.go +++ b/internal/net/http/accesslog/filter.go @@ -7,6 +7,7 @@ import ( E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/net/types" + "github.com/yusing/go-proxy/internal/utils/strutils" ) type ( @@ -48,8 +49,9 @@ func (method HTTPMethod) Fulfill(req *http.Request, res *http.Response) bool { return req.Method == string(method) } +// Parse implements strutils.Parser. func (k *HTTPHeader) Parse(v string) error { - split := strings.Split(v, "=") + split := strutils.SplitRune(v, '=') switch len(split) { case 1: split = append(split, "") diff --git a/internal/net/http/accesslog/status_code_range.go b/internal/net/http/accesslog/status_code_range.go index 01868446..599f1191 100644 --- a/internal/net/http/accesslog/status_code_range.go +++ b/internal/net/http/accesslog/status_code_range.go @@ -2,9 +2,9 @@ package accesslog import ( "strconv" - "strings" E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/utils/strutils" ) type StatusCodeRange struct { @@ -18,8 +18,9 @@ func (r *StatusCodeRange) Includes(code int) bool { return r.Start <= code && code <= r.End } +// Parse implements strutils.Parser. func (r *StatusCodeRange) Parse(v string) error { - split := strings.Split(v, "-") + split := strutils.SplitRune(v, '-') switch len(split) { case 1: start, err := strconv.Atoi(split[0]) diff --git a/internal/net/http/middleware/cloudflare_real_ip.go b/internal/net/http/middleware/cloudflare_real_ip.go index bd757702..1a78580c 100644 --- a/internal/net/http/middleware/cloudflare_real_ip.go +++ b/internal/net/http/middleware/cloudflare_real_ip.go @@ -6,7 +6,6 @@ import ( "io" "net" "net/http" - "strings" "sync" "time" @@ -113,7 +112,7 @@ func fetchUpdateCFIPRange(endpoint string, cfCIDRs *[]*types.CIDR) error { return err } - for _, line := range strings.Split(string(body), "\n") { + for _, line := range strutils.SplitLine(string(body)) { if line == "" { continue } diff --git a/internal/net/http/reverse_proxy_mod.go b/internal/net/http/reverse_proxy_mod.go index 47b008d1..b0537ef6 100644 --- a/internal/net/http/reverse_proxy_mod.go +++ b/internal/net/http/reverse_proxy_mod.go @@ -30,6 +30,7 @@ import ( "github.com/yusing/go-proxy/internal/net/http/accesslog" "github.com/yusing/go-proxy/internal/net/types" U "github.com/yusing/go-proxy/internal/utils" + "github.com/yusing/go-proxy/internal/utils/strutils" "golang.org/x/net/http/httpguts" ) @@ -528,7 +529,7 @@ func UpgradeType(h http.Header) string { func RemoveHopByHopHeaders(h http.Header) { // RFC 7230, section 6.1: Remove headers listed in the "Connection" header. for _, f := range h["Connection"] { - for _, sf := range strings.Split(f, ",") { + for _, sf := range strutils.SplitComma(f) { if sf = textproto.TrimString(sf); sf != "" { h.Del(sf) } diff --git a/internal/route/types/headers.go b/internal/route/types/headers.go index b2af83a2..2ec9ff85 100644 --- a/internal/route/types/headers.go +++ b/internal/route/types/headers.go @@ -2,17 +2,17 @@ package types import ( "net/http" - "strings" E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/utils/strutils" ) func ValidateHTTPHeaders(headers map[string]string) (http.Header, E.Error) { h := make(http.Header) for k, v := range headers { - vSplit := strings.Split(v, ",") + vSplit := strutils.CommaSeperatedList(v) for _, header := range vSplit { - h.Add(k, strings.TrimSpace(header)) + h.Add(k, header) } } return h, nil diff --git a/internal/route/types/raw_entry.go b/internal/route/types/raw_entry.go index ce7796db..86c6e1b4 100644 --- a/internal/route/types/raw_entry.go +++ b/internal/route/types/raw_entry.go @@ -173,14 +173,14 @@ func (e *RawEntry) Finalize() { } func (e *RawEntry) splitPorts() (lp string, pp string, extra string) { - portSplit := strings.Split(e.Port, ":") + portSplit := strutils.SplitRune(e.Port, ':') if len(portSplit) == 1 { pp = portSplit[0] } else { lp = portSplit[0] pp = portSplit[1] if len(portSplit) > 2 { - extra = strings.Join(portSplit[2:], ":") + extra = strutils.JoinRune(portSplit[2:], ':') } } return @@ -197,7 +197,7 @@ func joinPorts(lp string, pp string, extra string) string { if extra != "" { s = append(s, extra) } - return strings.Join(s, ":") + return strutils.JoinRune(s, ':') } func lowestPort(ports map[string]types.Port) string { diff --git a/internal/route/types/stream_port.go b/internal/route/types/stream_port.go index 3c1283cd..9cb6ae95 100644 --- a/internal/route/types/stream_port.go +++ b/internal/route/types/stream_port.go @@ -1,9 +1,8 @@ package types import ( - "strings" - E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/utils/strutils" ) type StreamPort struct { @@ -14,7 +13,7 @@ type StreamPort struct { var ErrStreamPortTooManyColons = E.New("too many colons") func ValidateStreamPort(p string) (StreamPort, error) { - split := strings.Split(p, ":") + split := strutils.SplitRune(p, ':') switch len(split) { case 1: diff --git a/internal/route/types/stream_scheme.go b/internal/route/types/stream_scheme.go index 968bf453..6f37161c 100644 --- a/internal/route/types/stream_scheme.go +++ b/internal/route/types/stream_scheme.go @@ -1,10 +1,8 @@ package types import ( - "fmt" - "strings" - E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/utils/strutils" ) type StreamScheme struct { @@ -14,7 +12,7 @@ type StreamScheme struct { func ValidateStreamScheme(s string) (*StreamScheme, error) { ss := &StreamScheme{} - parts := strings.Split(s, ":") + parts := strutils.SplitRune(s, ':') if len(parts) == 1 { parts = []string{s, s} } else if len(parts) != 2 { @@ -33,7 +31,7 @@ func ValidateStreamScheme(s string) (*StreamScheme, error) { } func (s StreamScheme) String() string { - return fmt.Sprintf("%s -> %s", s.ListeningScheme, s.ProxyScheme) + return string(s.ListeningScheme) + " -> " + string(s.ProxyScheme) } // IsCoherent checks if the ListeningScheme and ProxyScheme of the StreamScheme are equal. diff --git a/internal/utils/serialization.go b/internal/utils/serialization.go index 737f4c13..8b7c7c59 100644 --- a/internal/utils/serialization.go +++ b/internal/utils/serialization.go @@ -193,7 +193,7 @@ func Deserialize(src SerializedObject, dst any) E.Error { for _, field := range fields { var key string if jsonTag, ok := field.Tag.Lookup("json"); ok { - key = strings.Split(jsonTag, ",")[0] + key = strutils.CommaSeperatedList(jsonTag)[0] } else { key = field.Name } @@ -208,7 +208,7 @@ func Deserialize(src SerializedObject, dst any) E.Error { aliases, ok := field.Tag.Lookup("aliases") if ok { - for _, alias := range strings.Split(aliases, ",") { + for _, alias := range strutils.CommaSeperatedList(aliases) { mapping[alias] = dstV.FieldByName(field.Name) fieldName[field.Name] = alias } @@ -425,7 +425,7 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.E lines := []string{} src = strings.TrimSpace(src) if src != "" { - lines = strings.Split(src, "\n") + lines = strutils.SplitLine(src) for i := range lines { lines[i] = strings.TrimSpace(lines[i]) } diff --git a/internal/utils/strutils/parser.go b/internal/utils/strutils/parser.go index 42b199b6..fbf72ced 100644 --- a/internal/utils/strutils/parser.go +++ b/internal/utils/strutils/parser.go @@ -2,8 +2,6 @@ package strutils import ( "reflect" - - "github.com/yusing/go-proxy/internal/logging" ) type Parser interface { @@ -22,7 +20,7 @@ func Parse[T Parser](from string) (t T, err error) { func MustParse[T Parser](from string) T { t, err := Parse[T](from) if err != nil { - logging.Panic().Err(err).Msg("must failed") + panic("must failed: " + err.Error()) } return t } diff --git a/internal/utils/strutils/split_join.go b/internal/utils/strutils/split_join.go new file mode 100644 index 00000000..37b2d5c5 --- /dev/null +++ b/internal/utils/strutils/split_join.go @@ -0,0 +1,81 @@ +package strutils + +import ( + "math" + "strings" +) + +// SplitRune is like strings.Split but takes a rune as separator. +func SplitRune(s string, sep rune) []string { + if sep == 0 { + return strings.Split(s, "") + } + n := strings.Count(s, string(sep)) + 1 + if n > len(s)+1 { + n = len(s) + 1 + } + a := make([]string, n) + n-- + i := 0 + for i < n { + m := strings.IndexRune(s, sep) + if m < 0 { + break + } + a[i] = s[:m] + s = s[m+1:] + i++ + } + a[i] = s + return a[:i+1] +} + +// SplitComma is a wrapper around SplitRune(s, ','). +func SplitComma(s string) []string { + return SplitRune(s, ',') +} + +// SplitLine is a wrapper around SplitRune(s, '\n'). +func SplitLine(s string) []string { + return SplitRune(s, '\n') +} + +// SplitSpace is a wrapper around SplitRune(s, ' '). +func SplitSpace(s string) []string { + return SplitRune(s, ' ') +} + +// JoinRune is like strings.Join but takes a rune as separator. +func JoinRune(elems []string, sep rune) string { + switch len(elems) { + case 0: + return "" + case 1: + return elems[0] + } + if sep == 0 { + return strings.Join(elems, "") + } + + var n int + for _, elem := range elems { + if len(elem) > math.MaxInt-n { + panic("strings: Join output length overflow") + } + n += len(elem) + } + + var b strings.Builder + b.Grow(n) + b.WriteString(elems[0]) + for _, s := range elems[1:] { + b.WriteRune(sep) + b.WriteString(s) + } + return b.String() +} + +// JoinLines is a wrapper around JoinRune(elems, '\n'). +func JoinLines(elems []string) string { + return JoinRune(elems, '\n') +} diff --git a/internal/utils/strutils/split_join_test.go b/internal/utils/strutils/split_join_test.go new file mode 100644 index 00000000..96e2fe69 --- /dev/null +++ b/internal/utils/strutils/split_join_test.go @@ -0,0 +1,38 @@ +package strutils_test + +import ( + "strings" + "testing" + + . "github.com/yusing/go-proxy/internal/utils/strutils" + . "github.com/yusing/go-proxy/internal/utils/testing" +) + +var alphaNumeric = func() string { + var s strings.Builder + for i := range 'z' - 'a' + 1 { + s.WriteRune('a' + i) + s.WriteRune('A' + i) + s.WriteRune(',') + } + for i := range '9' - '0' + 1 { + s.WriteRune('0' + i) + s.WriteRune(',') + } + return s.String() +}() + +func TestSplit(t *testing.T) { + tests := map[string]rune{ + "": 0, + "1": '1', + ",": ',', + } + for sep, rsep := range tests { + t.Run(sep, func(t *testing.T) { + expected := strings.Split(alphaNumeric, sep) + ExpectDeepEqual(t, SplitRune(alphaNumeric, rsep), expected) + ExpectEqual(t, JoinRune(expected, rsep), alphaNumeric) + }) + } +} diff --git a/internal/utils/strutils/strconv.go b/internal/utils/strutils/strconv.go index a6979dcb..95e539eb 100644 --- a/internal/utils/strutils/strconv.go +++ b/internal/utils/strutils/strconv.go @@ -1,17 +1,7 @@ package strutils import ( - "errors" "strconv" - - E "github.com/yusing/go-proxy/internal/error" ) -func Atoi(s string) (int, E.Error) { - val, err := strconv.Atoi(s) - if err != nil { - return val, E.From(errors.Unwrap(err)).Subject(s) - } - - return val, nil -} +var Atoi = strconv.Atoi diff --git a/internal/utils/strutils/string.go b/internal/utils/strutils/string.go index f420e71b..03c47c85 100644 --- a/internal/utils/strutils/string.go +++ b/internal/utils/strutils/string.go @@ -8,8 +8,10 @@ import ( "golang.org/x/text/language" ) +// CommaSeperatedList returns a list of strings split by commas, +// then trim spaces from each element. func CommaSeperatedList(s string) []string { - res := strings.Split(s, ",") + res := SplitComma(s) for i, part := range res { res[i] = strings.TrimSpace(part) } diff --git a/internal/watcher/health/monitor/monitor.go b/internal/watcher/health/monitor/monitor.go index 49cd071b..3e41a584 100644 --- a/internal/watcher/health/monitor/monitor.go +++ b/internal/watcher/health/monitor/monitor.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "time" "github.com/yusing/go-proxy/internal/common" @@ -141,7 +140,7 @@ func (mon *monitor) Uptime() time.Duration { // Name implements HealthMonitor. func (mon *monitor) Name() string { - parts := strings.Split(mon.service, "/") + parts := strutils.SplitRune(mon.service, '/') return parts[len(parts)-1] }