diff --git a/.gitignore b/.gitignore index 6e9af0b..d7b563c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ -/.idea/ -/go.work -/go.work.sum -/go.sum -/close/test/clsoe.exe -/net/http/session/carrierDb/test/test.exe +/.idea/ +/go.work +/go.work.sum +/go.sum +/close/test/clsoe.exe +/net/http/session/carrierDb/test/test.exe +/.claude/ +/runtime/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fe01f92 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,46 @@ +你是一个中文AI助手。你的思考和表达都以中文进行——所有解释、推理、建议、提问、总结都必须使用中文。 +技术内容处理规则: +- 代码、命令、变量名、函数名、API参数、路径、公式、专有名词 → 保留英文原文 +- 注释 → 可以使用中文 +- 代码展示 → 前有中文用途说明,后有中文逻辑解读 + 请确保回答清晰、自然,技术术语首次出现时可用中文括号注明英文原词,例如:应用程序编程接口(API)。 + +## Go 编译环境配置 + +当执行任何 Go 编译相关命令(`go build`、`go test`、`go mod download`、`go install` 等)时,**必须**先设置以下环境变量,确保所有写入操作都在项目 `runtime/.go/` 目录下集中管理: + +### Bash / Zsh + +```bash +mkdir -p runtime/.go/{cache,tmp,mod} && \ +GOCACHE=$(pwd)/runtime/.go/cache \ +GOTMPDIR=$(pwd)/runtime/.go/tmp \ +GOMODCACHE=$(pwd)/runtime/.go/mod \ +go test -v -cover ./... +``` + +### PowerShell + +```powershell +mkdir -p runtime\.go\cache, runtime\.go\tmp, runtime\.go\mod -Force; ` +$env:GOCACHE = "$(Get-Location)\runtime\.go\cache"; ` +$env:GOTMPDIR = "$(Get-Location)\runtime\.go\tmp"; ` +$env:GOMODCACHE = "$(Get-Location)\runtime\.go\mod"; ` +$env:GOTOOLCHAIN = "local"; ` +go build -tags "timetzdata dev" -o ./runtime/build/vis-radar-dev.exe ./cmd/vis-radar.go +``` + +> **必须包含 `$env:GOTOOLCHAIN = "local"`**:项目 go.mod 要求的 Go 版本若高于本机安装版本, +> Go 会默认自动下载对应工具链,其校验缓存写入用户目录(`C:\Users\\go\pkg\sumdb`), +> 在沙箱/受限环境下会以 Access is denied 失败。固定 `local` 后改用本机工具链, +> 版本差异报错时再显式升级本机 Go,而不是让编译过程悄悄联网下载。 + +### Cmd + +```cmd +mkdir runtime\.go\cache runtime\.go\tmp runtime\.go\mod 2>nul && set GOCACHE=%cd%\runtime\.go\cache && set GOTMPDIR=%cd%\runtime\.go\tmp && set GOMODCACHE=%cd%\runtime\.go\mod && set GOTOOLCHAIN=local && go test -v ./... +``` + +### 注意事项 +- 清理缓存:`rm -rf runtime/.go`(Bash)或 `Remove-Item -Recurse -Force runtime\.go`(PowerShell) +- `GOTOOLCHAIN=local` 三种 shell 均需设置;换新机器或升级 Go 后首次编译如报“go.mod requires go >= x.x”,请升级本机 Go 而非移除此变量 \ No newline at end of file diff --git a/dataType/bool.go b/dataType/bool.go index 89ff61d..9ca32e5 100644 --- a/dataType/bool.go +++ b/dataType/bool.go @@ -1,8 +1,10 @@ package dataType import ( + "bytes" "database/sql/driver" "encoding/json" + "strconv" "gorm.io/gorm" "gorm.io/gorm/schema" @@ -105,11 +107,37 @@ func (b Bool) MarshalJSON() ([]byte, error) { } func (b *Bool) UnmarshalJSON(data []byte) error { - var val bool - if err := json.Unmarshal(data, &val); err != nil { - return err + ds := bytes.TrimSpace(data) + if len(ds) == 0 || bytes.Equal(ds, []byte("null")) { + b.bool = false + return nil + } + switch ds[0] { + case '"': // JSON 字符串 + s, err := strconv.Unquote(string(ds)) + if err != nil { + return err + } + ok, err := tools.Any2bool(s) + if err != nil { + return err + } + b.bool = ok + case 't': // true + b.bool = true + case 'f': // false + b.bool = false + default: // JSON 数字 + f, err := strconv.ParseFloat(string(ds), 64) + if err != nil { + return err + } + ok, err := tools.Any2bool(f) + if err != nil { + return err + } + b.bool = ok } - b.bool = val return nil } diff --git a/dataType/bool_test.go b/dataType/bool_test.go new file mode 100644 index 0000000..ef65320 --- /dev/null +++ b/dataType/bool_test.go @@ -0,0 +1,93 @@ +package dataType + +import ( + "encoding/json" + "testing" +) + +func TestBool_UnmarshalJSON_VariousTypes(t *testing.T) { + cases := []struct { + name string + in string + want bool + }{ + {"true", `true`, true}, + {"false", `false`, false}, + {"digit one", `1`, true}, + {"digit zero", `0`, false}, + {"digit non-zero", `2`, true}, + {"string true", `"true"`, true}, + {"string false", `"false"`, false}, + {"string one", `"1"`, true}, + {"string zero", `"0"`, false}, + {"string empty", `""`, false}, + {"null", `null`, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var b Bool + if err := json.Unmarshal([]byte(c.in), &b); err != nil { + t.Fatalf("json.Unmarshal(%q) error: %v", c.in, err) + } + if b.Bool() != c.want { + t.Fatalf("Bool() = %v, want %v (input %q)", b.Bool(), c.want, c.in) + } + }) + } +} + +func TestBool_UnmarshalJSON_DefaultField(t *testing.T) { + // 嵌入到结构体,验证常规字段解码路径 + type holder struct { + Flag Bool `json:"flag"` + } + var h holder + raw := `{"flag":"1"}` + if err := json.Unmarshal([]byte(raw), &h); err != nil { + t.Fatalf("json.Unmarshal struct error: %v", err) + } + if !h.Flag.Bool() { + t.Fatalf("Flag should be true, got %v", h.Flag.Bool()) + } +} + +func TestBool_MarshalJSON_RoundTrip(t *testing.T) { + for _, want := range []bool{true, false} { + b := NewBool(want) + raw, err := json.Marshal(b) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + var back Bool + if err := json.Unmarshal(raw, &back); err != nil { + t.Fatalf("Unmarshal(%s) error: %v", raw, err) + } + if back.Bool() != want { + t.Fatalf("round trip: got %v, want %v (raw %s)", back.Bool(), want, raw) + } + } +} + +func TestBool_UnmarshalJSON_Invalid(t *testing.T) { + cases := []string{ + `[]`, // 数组 + `{}`, // 对象 + `"yes"`, // ParseBool 无法识别 + `"abc"`, // ParseBool 无法识别 + `[`, // 非法 JSON + `"unterminated`, // 非法 JSON + } + for _, in := range cases { + var b Bool + if err := json.Unmarshal([]byte(in), &b); err == nil { + t.Errorf("json.Unmarshal(%q) expected error, got nil", in) + } + } +} + +func TestBool_UnmarshalJSON_Empty(t *testing.T) { + var b Bool + if err := json.Unmarshal(nil, &b); err == nil && b.Bool() { + t.Fatalf("empty input should give false without error, got true") + } +} \ No newline at end of file diff --git a/safe/resource.go b/safe/resource.go index c1fa8b1..e1a735e 100644 --- a/safe/resource.go +++ b/safe/resource.go @@ -4,14 +4,31 @@ import ( "sync/atomic" ) +// Resource 基于 atomic.Pointer 的原子、无锁(lock-free)资源。 +// +// # 线程安全说明 +// +// 只要通过本类型的方法(Read/Write/Get/Set/Reset/Update/Swap 等)访问资源, +// 操作就是原子且线程安全的,且完全无锁(多个 goroutine 读可完美并行)。 +// +// 需要区分两种"不安全": +// - 如果调用方拿到 Read() 返回的 *T 指针后,在方法外部并发修改指针所指向的内容, +// 这一段改写不是原子的,会引发数据竞争。此时局部指针只应被单 goroutine 独占使用。 +// - "读出来 → 函数外比较/修改 → 再写回"这类 read-modify-write 拆分到多个方法调用, +// 不是原子的,多 goroutine 并发会丢更新。需要原子 RMW 时请用 Update。 +// +// 因此,纯读推荐 Get(值拷贝)或 Read(指针,需自行保证不使用),写路径推荐 Update。 +// +// 注意:本类型是通用原子资源,不做编译期数值约束,也未内置数值累加方法。 +// 若需要原子的数值累加(计数器等),请在业务端用标准库 sync/atomic 的整型专用 +// 方法(如 atomic.Int64.Add),或用本类型的 Update 实现 read-modify-write。 type Resource[T any] struct { _ noCopy resource atomic.Pointer[T] } -// NewResource 创建一个原子操作的资源 -// 注意这个资源不是线程安全的 -// 虽然原子操作不会出现数据竞争,但是数据读取出来后的比较不是线程安全的。可能真实值已经被其他线程修改了 +// NewResource 创建一个原子操作的资源。 +// resource 允许传入 nil,此时内部会存储 T 的零值。 func NewResource[T any](resource *T) *Resource[T] { s := &Resource[T]{} if resource == nil { @@ -23,10 +40,92 @@ func NewResource[T any](resource *T) *Resource[T] { return s } +// NewResourceOf 按值创建一个原子操作的资源。 +// 与 NewResource 的区别在于直接传入值,风格同 NewResourceMutex/NewResourceRWMutex。 +func NewResourceOf[T any](resource T) *Resource[T] { + return NewResource(&resource) +} + +// Read 返回当前资源的指针。 +// 返回值是原始指针,多 goroutine 并发读取指针本身是安全的; +// 但在外部对该指针指向的内容做并发修改并不安全,请谨慎使用或改用 Get。 func (s *Resource[T]) Read() *T { return s.resource.Load() } +// Ptr 是 Read 的别名,返回当前资源的原始指针。 +func (s *Resource[T]) Ptr() *T { + return s.Read() +} + +// Get 返回当前资源的拷贝值。 +// 比 Read 更安全:即使内部为空指针,也返回 T 的零值而不会 panic。 +func (s *Resource[T]) Get() T { + p := s.resource.Load() + if p == nil { + var zero T + return zero + } + return *p +} + +// Write 原子地写入新资源(指针)。 +// 允许写入 nil,此时后续 Get 会返回 T 的零值。 func (s *Resource[T]) Write(newResource *T) { s.resource.Store(newResource) } + +// Set 原子地按值写入新资源。 +// 等价于调用 Write(&v),但调用方无需手动取地址。 +func (s *Resource[T]) Set(v T) { + s.Write(&v) +} + +// Reset 原子地将资源重置为 T 的零值。 +func (s *Resource[T]) Reset() { + var zero T + s.Set(zero) +} + +// Update 原子地执行修改(read-modify-write)。 +// 通过对当前指针快照调用 fn 得到新值,再用 CompareAndSwap 写回; +// 若期间被其他 goroutine 修改则重试,直至成功。 +// +// 注意:每次重试都会重新读取最新值并重新调用 fn,因此 fn 应尽量是 +// 纯函数(只依赖入参、不修改外部状态),否则可能导致 CAS 反复失败甚至死循环。 +// 适用于低竞争热路径,不承诺 wait-free。 +func (s *Resource[T]) Update(fn func(t T) T) { + for { + old := s.Read() + var oldVal T + if old != nil { + oldVal = *old + } + newVal := fn(oldVal) + if s.resource.CompareAndSwap(old, &newVal) { + return + } + } +} + +// Swap 原子地写入新指针并返回旧指针。 +func (s *Resource[T]) Swap(newValue *T) *T { + return s.resource.Swap(newValue) +} + +// SwapVal 原子地按值写入并返回旧值。 +// 若旧指针为空,返回 T 的零值。 +func (s *Resource[T]) SwapVal(v T) T { + old := s.resource.Swap(&v) + if old == nil { + var zero T + return zero + } + return *old +} + +// CompareAndSwap 原子地比较并交换指针。 +// 仅当当前指针与 old 相等时,才替换为 new 并返回 true。 +func (s *Resource[T]) CompareAndSwap(old, new *T) bool { + return s.resource.CompareAndSwap(old, new) +} \ No newline at end of file diff --git a/safe/resource_test.go b/safe/resource_test.go new file mode 100644 index 0000000..91d9e22 --- /dev/null +++ b/safe/resource_test.go @@ -0,0 +1,217 @@ +package safe + +import ( + "sync" + "sync/atomic" + "testing" +) + +// TestResourceBasic 验证构造器与值语义方法的基本行为。 +func TestResourceBasic(t *testing.T) { + // 按值构造 + r := NewResourceOf(10) + if got := r.Get(); got != 10 { + t.Fatalf("Get() = %v, want 10", got) + } + if got := r.Read(); got == nil || *got != 10 { + t.Fatalf("Read() 指针内容 = %v, want 10", got) + } + if got := r.Ptr(); got == nil || *got != 10 { + t.Fatalf("Ptr() 指针内容 = %v, want 10", got) + } + + // Set / Reset + r.Set(20) + if got := r.Get(); got != 20 { + t.Fatalf("after Set, Get() = %v, want 20", got) + } + r.Reset() + if got := r.Get(); got != 0 { + t.Fatalf("after Reset, Get() = %v, want 0", got) + } +} + +// TestResourceNewNil 验证 NewResource(nil) 存储 T 的零值且不 panic。 +func TestResourceNewNil(t *testing.T) { + r := NewResource[int](nil) + if got := r.Get(); got != 0 { + t.Fatalf("NewResource(nil).Get() = %v, want 0", got) + } +} + +// TestResourceGetNilSafe 验证内部为 nil 指针时 Get 返回零值不 panic。 +func TestResourceGetNilSafe(t *testing.T) { + r := &Resource[int]{} + if got := r.Get(); got != 0 { + t.Fatalf("empty Resource Get() = %v, want 0", got) + } + // Write(nil) 后 Get 仍返回零值 + r.Write(nil) + if got := r.Get(); got != 0 { + t.Fatalf("Write(nil).Get() = %v, want 0", got) + } +} + +// TestResourceSwap 验证 Swap/SwapVal 返回旧值。 +func TestResourceSwap(t *testing.T) { + r := NewResourceOf(1) + + old := r.SwapVal(2) + if old != 1 { + t.Fatalf("SwapVal 旧值 = %v, want 1", old) + } + if got := r.Get(); got != 2 { + t.Fatalf("after SwapVal, Get() = %v, want 2", got) + } + + newPtr := new(int) + *newPtr = 3 + oldPtr := r.Swap(newPtr) + if oldPtr == nil || *oldPtr != 2 { + t.Fatalf("Swap 旧指针 = %v, want &2", oldPtr) + } + if got := r.Get(); got != 3 { + t.Fatalf("after Swap, Get() = %v, want 3", got) + } +} + +// TestResourceCompareAndSwap 验证 CAS 成功/失败分支。 +// 注意:atomic.Pointer.CompareAndSwap 比较的是指针身份而非值相等, +// 因此旧指针必须取自内部实际存储的指针。 +func TestResourceCompareAndSwap(t *testing.T) { + // 构造时保存内部存储的指针,CAS 用该指针才能成功。 + stored := new(int) + *stored = 5 + r := NewResource(stored) + + replacement := 6 + if ok := r.CompareAndSwap(stored, &replacement); !ok { + t.Fatalf("CAS 应当成功") + } + if got := r.Get(); got != 6 { + t.Fatalf("after CAS, Get() = %v, want 6", got) + } + + // 用旧指针(已被替换)再次 CAS 应当失败。 + stale := stored + if ok := r.CompareAndSwap(stale, &replacement); ok { + t.Fatalf("CAS 使用已失效旧指针应当失败") + } + + // 用指向相同值但不同地址的指针也应失败(身份不等)。 + valueSame := 6 + if ok := r.CompareAndSwap(&valueSame, &replacement); ok { + t.Fatalf("CAS 用不同地址但相同值的指针应当失败") + } +} + +// TestResourceUpdate 验证 Update 的原子修改。 +func TestResourceUpdate(t *testing.T) { + r := NewResourceOf(10) + r.Update(func(v int) int { return v + 5 }) + if got := r.Get(); got != 15 { + t.Fatalf("after Update, Get() = %v, want 15", got) + } +} + +// TestResourceUpdateConcurrent 并发验证 Update 的最终一致性。 +func TestResourceUpdateConcurrent(t *testing.T) { + r := NewResourceOf(0) + const goroutines = 16 + const iters = 1000 + var wg sync.WaitGroup + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + for j := 0; j < iters; j++ { + r.Update(func(v int) int { return v + 1 }) + } + }() + } + wg.Wait() + if got := r.Get(); got != goroutines*iters { + t.Fatalf("并发 Update 最终值 = %v, want %v", got, goroutines*iters) + } +} + +// TestResourceMixConcurrent 混合读写下验证快照操作与新值语义不 panic。 +func TestResourceMixConcurrent(t *testing.T) { + r := NewResourceOf(100) + const goroutines = 8 + var wg sync.WaitGroup + wg.Add(goroutines) + var reads atomic.Int64 + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + for j := 0; j < 500; j++ { + _ = r.Get() + reads.Add(1) + r.Update(func(v int) int { return v + 1 }) + _ = r.SwapVal(100) + } + }() + } + wg.Wait() + if reads.Load() == 0 { + t.Fatalf("未执行任何读操作") + } +} + +// ---------- 基准测试 ---------- + +// BenchmarkResourceRead 原子版读。 +func BenchmarkResourceRead(b *testing.B) { + r := NewResourceOf(1) + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = r.Get() + } +} + +// BenchmarkResourceWrite 原子版写。 +func BenchmarkResourceWrite(b *testing.B) { + r := NewResourceOf(1) + val := 2 + b.ReportAllocs() + for i := 0; i < b.N; i++ { + r.Write(&val) + } +} + +// BenchmarkResourceUpdate 原子版 RMW 修改。 +func BenchmarkResourceUpdate(b *testing.B) { + r := NewResourceOf(0) + b.ReportAllocs() + for i := 0; i < b.N; i++ { + r.Update(func(v int) int { return v + 1 }) + } +} + +// BenchmarkResourceRWMutexRead RWMutex 版读。 +func BenchmarkResourceRWMutexRead(b *testing.B) { + r := NewResourceRWMutex(1) + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = r.Read() + } +} + +// BenchmarkResourceRWMutexWrite RWMutex 版写。 +func BenchmarkResourceRWMutexWrite(b *testing.B) { + r := NewResourceRWMutex(1) + b.ReportAllocs() + for i := 0; i < b.N; i++ { + r.Write(2) + } +} + +// BenchmarkResourceRWMutexUpdate RWMutex 版改。 +func BenchmarkResourceRWMutexUpdate(b *testing.B) { + r := NewResourceRWMutex(0) + b.ReportAllocs() + for i := 0; i < b.N; i++ { + r.Update(func(v int) int { return v + 1 }) + } +} \ No newline at end of file diff --git a/tools/array.go b/tools/array.go index 6c52316..f2e2964 100644 --- a/tools/array.go +++ b/tools/array.go @@ -477,3 +477,14 @@ func Pluck[T any, R any](slice []T, extractor func(T) R) []R { } return result } + +// SplitString 把字符串按分隔符拆成切片,并自动 trim 两端空白 +// 空字符串返回 nil(与 strings.Split 行为一致) +// 与原 hndx.callback.task 中 parsePhones 的处理一致 +func SplitString(s, sep string) []string { + s = strings.TrimSpace(s) + if s == "" { + return nil + } + return strings.Split(s, sep) +}