chore: apply modernize analyzer fixes across codebase

Mechanical replacements suggested by golang.org/x/tools/.../modernize:
strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(),
range-over-int, new(expr), strings.Builder for hot += loops,
reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines.
This commit is contained in:
MHSanaei 2026-05-18 12:22:02 +02:00
parent 317c7cb4f1
commit 1ef494bcda
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
24 changed files with 69 additions and 94 deletions

View file

@ -69,11 +69,11 @@ func isIgnorableDuplicateColumnErr(err error, mdl any) bool {
if !strings.Contains(errMsg, dupPrefix) {
return false
}
idx := strings.Index(errMsg, dupPrefix)
if idx < 0 {
_, after, ok := strings.Cut(errMsg, dupPrefix)
if !ok {
return false
}
col := strings.TrimSpace(errMsg[idx+len(dupPrefix):])
col := strings.TrimSpace(after)
col = strings.Trim(col, "`\"[]")
if col == "" {
return false

4
go.mod
View file

@ -12,7 +12,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v1.8.0
github.com/mymmrac/telego v1.9.0
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/robfig/cron/v3 v3.0.1
@ -25,7 +25,7 @@ require (
golang.org/x/crypto v0.51.0
golang.org/x/sys v0.44.0
golang.org/x/text v0.37.0
google.golang.org/grpc v1.81.0
google.golang.org/grpc v1.81.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1

8
go.sum
View file

@ -130,8 +130,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow=
github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
github.com/mymmrac/telego v1.9.0 h1:ZUJxZaPx/1IgRvVb5lXnUB8FgW5rNYfRe6Q2EJ4OJ+Y=
github.com/mymmrac/telego v1.9.0/go.mod h1:tVEB7OqiOPx8elRk9+ETkwiDQrUhWSB2XmAKIY9KmWY=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@ -258,8 +258,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -207,9 +207,9 @@ func (s *Server) initRouter() (*gin.Engine, error) {
path := c.Request.URL.Path
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
assetsIndex := strings.Index(path, "/assets/")
if assetsIndex != -1 {
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
_, after, ok := strings.Cut(path, "/assets/")
if ok {
assetPath := after // +8 to skip "/assets/"
if assetPath != "" {
c.FileFromFS(assetPath, assetsFS)
c.Abort()

View file

@ -2,6 +2,7 @@ package sub
import (
"fmt"
"maps"
"strings"
"github.com/goccy/go-json"
@ -471,8 +472,6 @@ func cloneMap(src map[string]any) map[string]any {
return nil
}
dst := make(map[string]any, len(src))
for k, v := range src {
dst[k] = v
}
maps.Copy(dst, src)
return dst
}

View file

@ -3,6 +3,7 @@ package ldaputil
import (
"crypto/tls"
"fmt"
"slices"
"github.com/go-ldap/ldap/v3"
)
@ -82,13 +83,7 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
continue
}
val := e.GetAttributeValue(cfg.FlagField)
enabled := false
for _, t := range cfg.TruthyVals {
if val == t {
enabled = true
break
}
}
enabled := slices.Contains(cfg.TruthyVals, val)
if cfg.Invert {
enabled = !enabled
}

View file

@ -32,7 +32,7 @@ func TestSeq_NotConstant(t *testing.T) {
func TestNum_InRange(t *testing.T) {
for _, upper := range []int{1, 2, 10, 1000} {
for i := 0; i < 200; i++ {
for range 200 {
v := Num(upper)
if v < 0 || v >= upper {
t.Fatalf("Num(%d) returned %d, out of [0, %d)", upper, v, upper)

View file

@ -1,5 +1,4 @@
//go:build darwin
// +build darwin
package sys

View file

@ -1,5 +1,4 @@
//go:build linux
// +build linux
package sys

View file

@ -1,5 +1,4 @@
//go:build windows
// +build windows
package sys

View file

@ -32,8 +32,8 @@ func NewAPIController(g *gin.RouterGroup, customGeo *service.CustomGeoService) *
func (a *APIController) checkAPIAuth(c *gin.Context) {
auth := c.GetHeader("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
tok := strings.TrimPrefix(auth, "Bearer ")
if after, ok := strings.CutPrefix(auth, "Bearer "); ok {
tok := after
if a.apiTokenService.Match(tok) {
if u, err := a.userService.GetFirstUser(); err == nil {
session.SetAPIAuthUser(c, u)

View file

@ -27,7 +27,7 @@ func getRemoteIp(c *gin.Context) string {
}
if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
for _, part := range strings.Split(xff, ",") {
for part := range strings.SplitSeq(xff, ",") {
if ip, ok := extractTrustedIP(part); ok {
return ip
}
@ -50,7 +50,7 @@ func isTrustedProxy(ip string) bool {
}
trusted := trustedProxyCIDRs()
for _, value := range strings.Split(trusted, ",") {
for value := range strings.SplitSeq(trusted, ",") {
value = strings.TrimSpace(value)
if value == "" {
continue

View file

@ -195,7 +195,7 @@ func (s *AllSetting) CheckValid() error {
s.SubClashPath += "/"
}
for _, cidr := range strings.Split(s.TrustedProxyCIDRs, ",") {
for cidr := range strings.SplitSeq(s.TrustedProxyCIDRs, ",") {
cidr = strings.TrimSpace(cidr)
if cidr == "" {
continue

View file

@ -42,28 +42,24 @@ func TestAtomicBool_ConcurrentSettersExactlyOneTakeWins(t *testing.T) {
const readers = 20
var wg sync.WaitGroup
for i := 0; i < setters; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for range setters {
wg.Go(func() {
a.set()
}()
})
}
wg.Wait()
trueCount := 0
var rwg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < readers; i++ {
rwg.Add(1)
go func() {
defer rwg.Done()
for range readers {
rwg.Go(func() {
if a.takeAndReset() {
mu.Lock()
trueCount++
mu.Unlock()
}
}()
})
}
rwg.Wait()

View file

@ -2526,10 +2526,7 @@ func chunkStrings(s []string, size int) [][]string {
}
out := make([][]string, 0, (len(s)+size-1)/size)
for i := 0; i < len(s); i += size {
end := i + size
if end > len(s) {
end = len(s)
}
end := min(i+size, len(s))
out = append(out, s[i:end])
}
return out
@ -2543,10 +2540,7 @@ func chunkInts(s []int, size int) [][]int {
}
out := make([][]int, 0, (len(s)+size-1)/size)
for i := 0; i < len(s); i += size {
end := i + size
if end > len(s) {
end = len(s)
}
end := min(i+size, len(s))
out = append(out, s[i:end])
}
return out

View file

@ -213,7 +213,7 @@ func compareVersionStrings(a string, b string) (int, bool) {
if !okA || !okB {
return 0, false
}
for i := 0; i < len(aParts); i++ {
for i := range len(aParts) {
if aParts[i] > bParts[i] {
return 1, true
}

View file

@ -72,7 +72,7 @@ func inboundTransports(protocol model.Protocol, streamSettings, settings string)
// "udp", or "tcp,udp". if it's set, it wins outright.
if n, ok := st["network"].(string); ok && n != "" {
bits = 0
for _, part := range strings.Split(n, ",") {
for part := range strings.SplitSeq(n, ",") {
switch strings.TrimSpace(part) {
case "tcp":
bits |= transportTCP

View file

@ -56,7 +56,8 @@ func seedInboundConflictNode(t *testing.T, tag, listen string, port int, protoco
}
}
func intPtr(v int) *int { return &v }
//go:fix inline
func intPtr(v int) *int { return new(v) }
func TestInboundTransports(t *testing.T) {
cases := []struct {
@ -360,7 +361,7 @@ func TestGenerateInboundTag_SpecificListenSameDisambiguation(t *testing.T) {
func TestCheckPortConflict_NodeScope(t *testing.T) {
setupConflictDB(t)
seedInboundConflictNode(t, "local-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, nil)
seedInboundConflictNode(t, "node1-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, intPtr(1))
seedInboundConflictNode(t, "node1-443-tcp", "0.0.0.0", 443, model.VLESS, `{"network":"tcp"}`, `{}`, new(1))
svc := &InboundService{}
@ -370,8 +371,8 @@ func TestCheckPortConflict_NodeScope(t *testing.T) {
want bool
}{
{"new local same port + tcp clashes with local", nil, true},
{"new remote on different node from local is fine", intPtr(2), false},
{"new remote on existing node 1 clashes", intPtr(1), true},
{"new remote on different node from local is fine", new(2), false},
{"new remote on existing node 1 clashes", new(1), true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {

View file

@ -227,7 +227,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
parsedAdminIds := make([]int64, 0)
// Parse admin IDs from comma-separated string
if tgBotID != "" {
for _, adminID := range strings.Split(tgBotID, ",") {
for adminID := range strings.SplitSeq(tgBotID, ",") {
id, err := strconv.ParseInt(adminID, 10, 64)
if err != nil {
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
@ -2051,12 +2051,7 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
// checkAdmin checks if the given Telegram ID is an admin.
func checkAdmin(tgId int64) bool {
for _, adminId := range adminIds {
if adminId == tgId {
return true
}
}
return false
return slices.Contains(adminIds, tgId)
}
// SendAnswer sends a response message with an inline keyboard to the specified chat.
@ -2373,17 +2368,18 @@ func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
// Send in chunks to respect message length; use monospace formatting
const maxPerMessage = 50
for i := 0; i < len(cleaned); i += maxPerMessage {
j := i + maxPerMessage
if j > len(cleaned) {
j = len(cleaned)
}
j := min(i+maxPerMessage, len(cleaned))
chunk := cleaned[i:j]
msg := t.I18nBot("subscription.individualLinks") + ":\r\n"
var msg strings.Builder
msg.WriteString(t.I18nBot("subscription.individualLinks"))
msg.WriteString(":\r\n")
for _, link := range chunk {
// wrap each link in <code>
msg += "<code>" + link + "</code>\r\n"
msg.WriteString("<code>")
msg.WriteString(link)
msg.WriteString("</code>\r\n")
}
t.SendMsgToTgbot(chatId, msg)
t.SendMsgToTgbot(chatId, msg.String())
}
}
@ -3439,7 +3435,8 @@ func (t *Tgbot) notifyExhausted() {
var exhaustedClients []xray.ClientTraffic
traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID)
if err == nil && len(traffics) > 0 {
output := t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
var output strings.Builder
output.WriteString(t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")))
for _, traffic := range traffics {
if traffic.Enable {
if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) ||
@ -3451,21 +3448,23 @@ func (t *Tgbot) notifyExhausted() {
}
}
if len(exhaustedClients) > 0 {
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
output.WriteString(t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))))
if len(disabledClients) > 0 {
output += t.I18nBot("tgbot.clients") + ":\r\n"
output.WriteString(t.I18nBot("tgbot.clients"))
output.WriteString(":\r\n")
for _, traffic := range disabledClients {
output += " " + traffic.Email
output.WriteString(" ")
output.WriteString(traffic.Email)
}
output += "\r\n"
output.WriteString("\r\n")
}
output += "\r\n"
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients)))
output.WriteString("\r\n")
output.WriteString(t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients))))
for _, traffic := range exhaustedClients {
output += t.clientInfoMsg(&traffic, true, false, false, true, true, false)
output += "\r\n"
output.WriteString(t.clientInfoMsg(&traffic, true, false, false, true, true, false))
output.WriteString("\r\n")
}
t.SendMsgToTgbot(chatID, output)
t.SendMsgToTgbot(chatID, output.String())
}
chatIDsDone = append(chatIDsDone, chatID)
}
@ -3480,12 +3479,7 @@ func (t *Tgbot) notifyExhausted() {
// int64Contains checks if an int64 slice contains a specific item.
func int64Contains(slice []int64, item int64) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
return slices.Contains(slice, item)
}
// onlineClients retrieves and sends information about online clients.

View file

@ -6,7 +6,7 @@ import (
)
func TestLoginAttemptDoesNotCarryPassword(t *testing.T) {
typ := reflect.TypeOf(LoginAttempt{})
typ := reflect.TypeFor[LoginAttempt]()
if _, ok := typ.FieldByName("Password"); ok {
t.Fatal("LoginAttempt must not carry attempted passwords")
}

View file

@ -3,6 +3,7 @@ package service
import (
_ "embed"
"encoding/json"
"slices"
"github.com/mhsanaei/3x-ui/v3/util/common"
"github.com/mhsanaei/3x-ui/v3/xray"
@ -55,7 +56,7 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
// If `raw` does not look like a wrapper, it is returned unchanged.
func UnwrapXrayTemplateConfig(raw string) string {
const maxDepth = 8 // defensive cap against pathological multi-nest values
for i := 0; i < maxDepth; i++ {
for range maxDepth {
var top map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &top); err != nil {
return raw
@ -190,10 +191,8 @@ func findApiRule(rules []map[string]any) int {
}
}
case []string:
for _, s := range tags {
if s == "api" {
return i
}
if slices.Contains(tags, "api") {
return i
}
case string:
if tags == "api" {

View file

@ -65,7 +65,7 @@ func TestUnwrapXrayTemplateConfig(t *testing.T) {
// non-wrapped, and confirm we end up at some valid JSON (we
// don't loop forever and we don't blow the stack).
s := real
for i := 0; i < 16; i++ {
for range 16 {
s = `{"xraySetting":` + s + `}`
}
got := UnwrapXrayTemplateConfig(s)

View file

@ -110,7 +110,7 @@ func (h *Hub) shouldThrottle(msgType MessageType) bool {
// panic doesn't permanently kill real-time updates for commercial deployments.
// After the cap, the hub stays down and the frontend falls back to REST polling.
func (h *Hub) Run() {
for attempt := 0; attempt < hubRestartAttempts; attempt++ {
for attempt := range hubRestartAttempts {
stopped := h.runOnce()
if stopped {
return

View file

@ -231,7 +231,7 @@ func TestHub_ConcurrentRegisterUnregister(t *testing.T) {
const n = 50
var wg sync.WaitGroup
for i := 0; i < n; i++ {
for i := range n {
wg.Add(1)
go func(idx int) {
defer wg.Done()