From d5bb1dfd82fe0d48224e403f1ff38f550a62e42a Mon Sep 17 00:00:00 2001 From: longyue0521 Date: Mon, 17 Apr 2023 12:35:03 +0800 Subject: [PATCH 01/32] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84task=20pool?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pool/task_pool_test.go | 320 +++++++++++++++-------------------------- 1 file changed, 114 insertions(+), 206 deletions(-) diff --git a/pool/task_pool_test.go b/pool/task_pool_test.go index 95379a67..95f74a54 100644 --- a/pool/task_pool_test.go +++ b/pool/task_pool_test.go @@ -29,254 +29,151 @@ import ( func TestOnDemandBlockTaskPool_States(t *testing.T) { t.Parallel() - t.Run("ctx canceled", func(t *testing.T) { - p1, err := NewOnDemandBlockTaskPool(2, 5) - assert.NoError(t, err) - testTaskPoolStatesCtxCanceled(t, p1, context.Canceled) - }) - t.Run("shutdownNowCtx canceled", func(t *testing.T) { - p1, err := NewOnDemandBlockTaskPool(2, 5) - assert.NoError(t, err) - testTaskPoolStatesShutdownNowCtxCanceled(t, p1, context.Canceled) - }) + t.Run("调用States方法时使用已取消的context应该返回错误", func(t *testing.T) { + t.Parallel() - t.Run("shutdownCtx canceled", func(t *testing.T) { - p1, err := NewOnDemandBlockTaskPool(2, 5) + pool, err := NewOnDemandBlockTaskPool(1, 3) assert.NoError(t, err) - testTaskPoolStatesShutdownCtxCanceled(t, p1, context.Canceled) - }) - t.Run("ctx Running canceled", func(t *testing.T) { - p2, err := NewOnDemandBlockTaskPool(2, 5) - assert.NoError(t, err) - testTaskPoolStatesCtxRunningCanceled(t, p2, - State{PoolState: stateRunning, GoCnt: 2, - WaitingTasksCnt: 3, QueueSize: 5, RunningTasksCnt: 2}) - }) + ctx, cancel := context.WithCancel(context.Background()) + cancel() - t.Run("pool not running", func(t *testing.T) { - p, err := NewOnDemandBlockTaskPool(2, 5) - assert.NoError(t, err) - testTaskPoolStatesPoolNotRunning(t, p, - State{PoolState: stateCreated, GoCnt: 0, WaitingTasksCnt: 5, QueueSize: 5, RunningTasksCnt: 0}) + _, err = pool.States(ctx, time.Millisecond) + assert.Equal(t, context.Canceled, err) }) - t.Run("pool Shutdown", func(t *testing.T) { - p, err := NewOnDemandBlockTaskPool(2, 5) + t.Run("调用ShutdownNow方法后再调用States方法应该返回错误", func(t *testing.T) { + t.Parallel() + + pool, err := NewOnDemandBlockTaskPool(1, 3) assert.NoError(t, err) - testTaskPoolStatesPoolShutdown(t, p, - State{PoolState: stateClosing, GoCnt: 2, WaitingTasksCnt: 3, QueueSize: 5, RunningTasksCnt: 2}, - State{PoolState: stateStopped, GoCnt: 0, WaitingTasksCnt: 0, QueueSize: 5, RunningTasksCnt: 0}) - }) - t.Run("pool Shutdown Now", func(t *testing.T) { - p, err := NewOnDemandBlockTaskPool(1, 2) + err = pool.Start() assert.NoError(t, err) - testTaskPoolStatesPoolShutdownNow(t, p) - }) -} -func testTaskPoolStatesCtxCanceled(t *testing.T, pool *OnDemandBlockTaskPool, wantErr error) { - done := make(chan struct{}) - err := pool.Submit(context.Background(), TaskFunc(func(ctx context.Context) error { - <-done - return nil - })) - assert.NoError(t, err) + _, err = pool.ShutdownNow() + assert.NoError(t, err) - err = pool.Start() - assert.NoError(t, err) + _, err = pool.States(context.Background(), time.Millisecond) + assert.Equal(t, context.Canceled, err) + }) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - cancel() - _, err = pool.States(ctx, time.Millisecond) - assert.Equal(t, wantErr, err) - close(done) -} + t.Run("调用Shutdown方法后再调用States方法应该返回错误", func(t *testing.T) { + t.Parallel() -func testTaskPoolStatesShutdownNowCtxCanceled(t *testing.T, pool *OnDemandBlockTaskPool, wantErr error) { - done := make(chan struct{}) - err := pool.Submit(context.Background(), TaskFunc(func(ctx context.Context) error { - <-done - return nil - })) - assert.NoError(t, err) + pool, err := NewOnDemandBlockTaskPool(1, 3) + assert.NoError(t, err) - err = pool.Start() - assert.NoError(t, err) - done <- struct{}{} - _, err = pool.ShutdownNow() - assert.NoError(t, err) + err = pool.Start() + assert.NoError(t, err) - _, err = pool.States(context.Background(), time.Millisecond) - assert.Equal(t, wantErr, err) - close(done) -} + done, err := pool.Shutdown() + assert.NoError(t, err) -func testTaskPoolStatesShutdownCtxCanceled(t *testing.T, pool *OnDemandBlockTaskPool, wantErr error) { - done := make(chan struct{}) - err := pool.Submit(context.Background(), TaskFunc(func(ctx context.Context) error { <-done - return nil - })) - assert.NoError(t, err) - - err = pool.Start() - assert.NoError(t, err) - // 当 queue 里的任务为 0 个时, 调用 Shutdown() 并不会执行相应的 cancel - //done <- struct{}{} - _, err = pool.Shutdown() - assert.NoError(t, err) - done <- struct{}{} - - _, err = pool.States(context.Background(), time.Millisecond) - assert.Equal(t, wantErr, err) - close(done) -} + _, err = pool.States(context.Background(), time.Millisecond) + assert.Equal(t, context.Canceled, err) + }) -func testTaskPoolStatesCtxRunningCanceled(t *testing.T, pool *OnDemandBlockTaskPool, wantState State) { - err := pool.Start() - assert.NoError(t, err) + t.Run("调用States方法返回的chan应该能够正常读取数据", func(t *testing.T) { + t.Parallel() - done := make(chan struct{}) - n := cap(pool.queue) + pool, err := NewOnDemandBlockTaskPool(1, 3) + assert.NoError(t, err) - for i := 0; i < n; i++ { - err = pool.Submit(context.Background(), TaskFunc(func(ctx context.Context) error { - <-done - return nil - })) + ch, err := pool.States(context.Background(), time.Millisecond) assert.NoError(t, err) - } + assert.NotZero(t, <-ch) + }) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - ch, err := pool.States(ctx, time.Millisecond) - assert.NoError(t, err) - state1 := <-ch - assert.Equal(t, wantState.PoolState, state1.PoolState) - assert.Equal(t, wantState.QueueSize, state1.QueueSize) - assert.Equal(t, wantState.GoCnt, state1.GoCnt) - assert.Equal(t, wantState.WaitingTasksCnt, state1.WaitingTasksCnt) - assert.Equal(t, wantState.RunningTasksCnt, state1.RunningTasksCnt) - - cancel() - for { - state2, ok := <-ch - if !ok { - break - } - assert.Equal(t, wantState.PoolState, state2.PoolState) - assert.Equal(t, wantState.QueueSize, state2.QueueSize) - assert.Equal(t, wantState.GoCnt, state2.GoCnt) - assert.Equal(t, wantState.WaitingTasksCnt, state2.WaitingTasksCnt) - assert.Equal(t, wantState.RunningTasksCnt, state2.RunningTasksCnt) - } - close(done) -} + t.Run("当调用States方法时传入的context超时返回的chan应该被关闭", func(t *testing.T) { + t.Parallel() -func testTaskPoolStatesPoolNotRunning(t *testing.T, pool *OnDemandBlockTaskPool, wantState State) { - done := make(chan struct{}) - n := cap(pool.queue) + initGo, queueSize := 1, 3 + pool, syncChan := testNewRunningStateTaskPoolWithQueueFullFilled(t, initGo, queueSize) - for i := 0; i < n; i++ { - err := pool.Submit(context.Background(), TaskFunc(func(ctx context.Context) error { - <-done - return nil - })) + ctx, cancel := context.WithCancel(context.Background()) + ch, err := pool.States(ctx, time.Millisecond) assert.NoError(t, err) - } - ch, err := pool.States(context.Background(), time.Millisecond) - assert.NoError(t, err) - state1 := <-ch - assert.Equal(t, wantState.PoolState, state1.PoolState) - assert.Equal(t, wantState.QueueSize, state1.QueueSize) - assert.Equal(t, wantState.GoCnt, state1.GoCnt) - assert.Equal(t, wantState.WaitingTasksCnt, state1.WaitingTasksCnt) - assert.Equal(t, wantState.RunningTasksCnt, state1.RunningTasksCnt) - close(done) -} + go func() { + // simulate timeout + <-time.After(3 * time.Millisecond) + cancel() + }() -func testTaskPoolStatesPoolShutdown(t *testing.T, pool *OnDemandBlockTaskPool, closingState, stoppedState State) { - done := make(chan struct{}) - n := cap(pool.queue) + for { + state, ok := <-ch + if !ok { + break + } + assert.NotZero(t, state) + } - for i := 0; i < n; i++ { - err := pool.Submit(context.Background(), TaskFunc(func(ctx context.Context) error { - <-done - return nil - })) + // clean up + close(syncChan) + _, err = pool.Shutdown() assert.NoError(t, err) - } + }) - err := pool.Start() - assert.NoError(t, err) + t.Run("调用Shutdown方法应该关闭States方法返回的chan", func(t *testing.T) { + t.Parallel() - _, err = pool.Shutdown() - assert.NoError(t, err) + pool := testNewRunningStateTaskPool(t, 1, 3) - ch, err := pool.States(context.Background(), time.Millisecond) - assert.NoError(t, err) - state1 := <-ch - assert.Equal(t, closingState.PoolState, state1.PoolState) - assert.Equal(t, closingState.QueueSize, state1.QueueSize) - assert.Equal(t, closingState.GoCnt, state1.GoCnt) - assert.Equal(t, closingState.WaitingTasksCnt, state1.WaitingTasksCnt) - assert.Equal(t, closingState.RunningTasksCnt, state1.RunningTasksCnt) + ch, err := pool.States(context.Background(), time.Millisecond) + assert.NoError(t, err) - close(done) - for { - state2, ok := <-ch - if !ok { - break + go func() { + time.Sleep(5 * time.Millisecond) + _, err := pool.Shutdown() + assert.NoError(t, err) + }() + + for { + state, ok := <-ch + if !ok { + break + } + assert.NotZero(t, state) } - assert.Equal(t, stoppedState.PoolState, state2.PoolState) - assert.Equal(t, stoppedState.QueueSize, state2.QueueSize) - assert.Equal(t, stoppedState.GoCnt, state2.GoCnt) - assert.Equal(t, stoppedState.WaitingTasksCnt, state2.WaitingTasksCnt) - assert.Equal(t, stoppedState.RunningTasksCnt, state2.RunningTasksCnt) - } -} + }) -func testTaskPoolStatesPoolShutdownNow(t *testing.T, pool *OnDemandBlockTaskPool) { - done := make(chan struct{}) - err := pool.Submit(context.Background(), TaskFunc(func(ctx context.Context) error { - <-done - return nil - })) - assert.NoError(t, err) + t.Run("调用ShutdownNow方法应该关闭States方法返回的chan", func(t *testing.T) { + t.Parallel() - err = pool.Start() - assert.NoError(t, err) + pool := testNewRunningStateTaskPool(t, 1, 3) - ch, err := pool.States(context.Background(), time.Millisecond) - assert.NoError(t, err) - done <- struct{}{} - _, err = pool.ShutdownNow() - assert.NoError(t, err) + ch, err := pool.States(context.Background(), time.Millisecond) + assert.NoError(t, err) - for { - state, ok := <-ch - if !ok { - break - } - assert.Equal(t, stateStopped, state.PoolState) - } + go func() { + time.Sleep(5 * time.Millisecond) + _, err := pool.ShutdownNow() + assert.NoError(t, err) + }() - close(done) + for { + state, ok := <-ch + if !ok { + break + } + assert.NotZero(t, state) + } + }) } /* TaskPool有限状态机 Start/Submit/ShutdownNow() Error \ / - Shutdown() --> CLOSING ---等待所有任务结束 - Submit()nil--执行中状态迁移--Submit() / \----------/ \----------/ - \ / \ / / -New() --> CREATED -- Start() ---> RUNNING -- -- - \ / \ / \ Start/Submit/Shutdown() Error - Shutdown/ShutdownNow()Error Start() \ \ / + Shutdown() --> CLOSING ---等待所有任务结束 ----\ + Submit()nil--执行中状态迁移--Submit() / \----------/ \ + \ / \ / / \ +New() --> CREATED -- Start() ---> RUNNING -- -- \ + \ / \ / \ Start/Submit/Shutdown() Error \ + Shutdown/ShutdownNow()Error Start() \ \ / \ ShutdownNow() ---> STOPPED -- ShutdownNow() --> STOPPED */ @@ -477,8 +374,10 @@ func TestOnDemandBlockTaskPool_In_Running_State(t *testing.T) { }) t.Run("Start —— 在TaskPool启动前队列中已有任务,启动后不再Submit", func(t *testing.T) { + t.Parallel() t.Run("WithCoreGo,WithMaxIdleTime,所需要协程数 <= 允许创建的协程数", func(t *testing.T) { + t.Parallel() initGo, coreGo, maxIdleTime := 1, 3, 3*time.Millisecond queueSize := coreGo @@ -511,9 +410,12 @@ func TestOnDemandBlockTaskPool_In_Running_State(t *testing.T) { <-wait } assert.Equal(t, int32(coreGo), pool.numOfGo()) + close(done) }) t.Run("WithMaxGo, 所需要协程数 > 允许创建的协程数", func(t *testing.T) { + t.Parallel() + initGo, maxGo := 3, 5 queueSize := maxGo + 1 @@ -545,10 +447,12 @@ func TestOnDemandBlockTaskPool_In_Running_State(t *testing.T) { <-wait } assert.Equal(t, int32(maxGo), pool.numOfGo()) + close(done) }) }) t.Run("Start —— 与Submit并发调用,WithCoreGo,WithMaxIdleTime,WithMaxGo,所需要协程数 < 允许创建的协程数", func(t *testing.T) { + t.Parallel() initGo, coreGo, maxGo, maxIdleTime := 2, 4, 6, 3*time.Millisecond queueSize := coreGo @@ -588,6 +492,7 @@ func TestOnDemandBlockTaskPool_In_Running_State(t *testing.T) { } assert.Equal(t, int32(maxGo), pool.numOfGo()) + close(done) }) t.Run("Submit", func(t *testing.T) { @@ -896,21 +801,21 @@ func TestOnDemandBlockTaskPool_In_Closing_State(t *testing.T) { pool := testNewRunningStateTaskPool(t, initGo, queueSize) // 模拟阻塞提交 - n := initGo + queueSize*2 + n := initGo + queueSize + 1 eg := new(errgroup.Group) - waitChan := make(chan struct{}, n) + waitChan := make(chan struct{}) taskDone := make(chan struct{}) for i := 0; i < n; i++ { eg.Go(func() error { return pool.Submit(context.Background(), TaskFunc(func(ctx context.Context) error { - waitChan <- struct{}{} + <-waitChan <-taskDone return nil })) }) } for i := 0; i < initGo; i++ { - <-waitChan + waitChan <- struct{}{} } done, err := pool.Shutdown() assert.NoError(t, err) @@ -925,6 +830,7 @@ func TestOnDemandBlockTaskPool_In_Closing_State(t *testing.T) { assert.Equal(t, int32(initGo), pool.numOfGo()) + close(waitChan) close(taskDone) <-done assert.Equal(t, stateStopped, pool.internalState()) @@ -1204,6 +1110,8 @@ func testNewRunningStateTaskPoolWithQueueFullFilled(t *testing.T, initGo int, qu } func TestGroup(t *testing.T) { + t.Parallel() + n := 10 // g := &sliceGroup{members: make([]int, n, n)} From 092aecd4ba717ba6aa52187c6984ff8d1d1318f0 Mon Sep 17 00:00:00 2001 From: longyue0521 Date: Mon, 17 Apr 2023 12:58:39 +0800 Subject: [PATCH 02/32] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=9C=89?= =?UTF-8?q?=E9=99=90=E7=8A=B6=E6=80=81=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pool/task_pool_test.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pool/task_pool_test.go b/pool/task_pool_test.go index 95f74a54..b956c9f9 100644 --- a/pool/task_pool_test.go +++ b/pool/task_pool_test.go @@ -166,15 +166,17 @@ func TestOnDemandBlockTaskPool_States(t *testing.T) { /* TaskPool有限状态机 - Start/Submit/ShutdownNow() Error - \ / - Shutdown() --> CLOSING ---等待所有任务结束 ----\ - Submit()nil--执行中状态迁移--Submit() / \----------/ \ - \ / \ / / \ -New() --> CREATED -- Start() ---> RUNNING -- -- \ - \ / \ / \ Start/Submit/Shutdown() Error \ - Shutdown/ShutdownNow()Error Start() \ \ / \ - ShutdownNow() ---> STOPPED -- ShutdownNow() --> STOPPED + Start/Submit/Shutdown/ShutdownNow() Error + \ / + Shutdown() --> CLOSING --> 等待所有任务结束 + States/Submit()---执行中状态迁移--States/Submit() / \ / | + \ / \ / / States() | +New() ---> CREATED ----- Start() ------> RUNNING ------ | + \ / \ / \ | + Shutdown/ShutdownNow()Error Start() \ | + ShutdownNow() ---> STOPPED <-------- | + \ / + Start/Submit/Shutdown/ShutdownNow/States() Error */ func TestOnDemandBlockTaskPool_In_Created_State(t *testing.T) { From 89d07ad61a1aa4330b04a1b67cc8cb271c0c99df Mon Sep 17 00:00:00 2001 From: longyue0521 Date: Mon, 17 Apr 2023 13:03:09 +0800 Subject: [PATCH 03/32] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=9C=89?= =?UTF-8?q?=E9=99=90=E7=8A=B6=E6=80=81=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pool/task_pool_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pool/task_pool_test.go b/pool/task_pool_test.go index b956c9f9..bc9cbabd 100644 --- a/pool/task_pool_test.go +++ b/pool/task_pool_test.go @@ -169,10 +169,10 @@ TaskPool有限状态机 Start/Submit/Shutdown/ShutdownNow() Error \ / Shutdown() --> CLOSING --> 等待所有任务结束 - States/Submit()---执行中状态迁移--States/Submit() / \ / | - \ / \ / / States() | + States/Submit()---执行中状态迁移--States/Submit() / \ / | + \ / \ / / States() | New() ---> CREATED ----- Start() ------> RUNNING ------ | - \ / \ / \ | + \ / \ / \ | Shutdown/ShutdownNow()Error Start() \ | ShutdownNow() ---> STOPPED <-------- | \ / From 07bb8d139ca544352aa4af6c491874f8ed5f1e28 Mon Sep 17 00:00:00 2001 From: longyue0521 Date: Mon, 17 Apr 2023 13:10:46 +0800 Subject: [PATCH 04/32] =?UTF-8?q?=E4=BF=AE=E6=94=B9CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index b1c493f0..218f3485 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,4 +1,7 @@ # 开发中 +- [pool: 重构TaskPool测试用例](https://github.com/ecodeclub/ekit/pull/178) + +# v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) - [reflectx: IsNil 方法](https://github.com/ecodeclub/ekit/pull/150) - [setx: Set接口设计和基于map的实现](https://github.com/ecodeclub/ekit/pull/148) @@ -8,6 +11,7 @@ - [pool: TaskPool 的可观测性](https://github.com/ecodeclub/ekit/pull/166) - [retry: Strategy 接口设计与等间隔重试实现](https://github.com/ecodeclub/ekit/pull/169) - [retry: 指数退避重试策略实现](https://github.com/ecodeclub/ekit/pull/174) + # v0.0.6 - [queue: 基于semaphore的并发阻塞队列实现](https://github.com/ecodeclub/ekit/pull/129) - [mapx: hashmap实现](https://github.com/ecodeclub/ekit/pull/132) From 9ea568d5ce7eadcc98a88332faca6406b97cd6b5 Mon Sep 17 00:00:00 2001 From: hookokoko <648646891@qq.com> Date: Tue, 9 May 2023 12:18:22 +0800 Subject: [PATCH 05/32] =?UTF-8?q?sqlx:=20=E6=B7=BB=E5=8A=A0ScanRows=20?= =?UTF-8?q?=E5=92=8C=20ScanAll=E6=96=B9=E6=B3=95=20(#180)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .CHANGELOG.md | 1 + sqlx/scan.go | 58 ++++++++++++++++ sqlx/scan_test.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 sqlx/scan.go create mode 100644 sqlx/scan_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 218f3485..1a646b77 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,5 +1,6 @@ # 开发中 - [pool: 重构TaskPool测试用例](https://github.com/ecodeclub/ekit/pull/178) +- [sqlx:ScanRows 和 ScanAll方法](https://github.com/ecodeclub/ekit/pull/180) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/sqlx/scan.go b/sqlx/scan.go new file mode 100644 index 00000000..3b0b1baf --- /dev/null +++ b/sqlx/scan.go @@ -0,0 +1,58 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlx + +import ( + "database/sql" + "reflect" +) + +func ScanRows(rows *sql.Rows) ([]any, error) { + colsInfo, err := rows.ColumnTypes() + if err != nil { + return nil, err + } + colsData := make([]any, 0, len(colsInfo)) + for _, colInfo := range colsInfo { + typ := colInfo.ScanType() + // 保险起见,循环的去除指针 + for typ.Kind() == reflect.Pointer { + typ = typ.Elem() + } + newData := reflect.New(typ).Interface() + colsData = append(colsData, newData) + } + err = rows.Scan(colsData...) + if err != nil { + return nil, err + } + // 去掉reflect.New的指针 + for i := 0; i < len(colsData); i++ { + colsData[i] = reflect.ValueOf(colsData[i]).Elem().Interface() + } + return colsData, nil +} + +func ScanAll(rows *sql.Rows) ([][]any, error) { + res := make([][]any, 0, 32) + for rows.Next() { + cols, err := ScanRows(rows) + if err != nil { + return nil, err + } + res = append(res, cols) + } + return res, nil +} diff --git a/sqlx/scan_test.go b/sqlx/scan_test.go new file mode 100644 index 00000000..7622ded8 --- /dev/null +++ b/sqlx/scan_test.go @@ -0,0 +1,174 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlx + +import ( + "context" + "database/sql" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScanRows(t *testing.T) { + db, err := sql.Open("sqlite3", "file:test01.db?cache=shared&mode=memory") + if err != nil { + t.Error(err) + } + defer db.Close() + + query := "DROP TABLE IF EXISTS t1; CREATE TABLE t1 (\n id int primary key,\n `int` int,\n `integer` integer,\n `tinyint` TINYINT,\n `smallint` smallint,\n `MEDIUMINT` MEDIUMINT,\n `BIGINT` BIGINT,\n `UNSIGNED_BIG_INT` UNSIGNED BIG INT,\n `INT2` INT2,\n `INT8` INT8,\n `VARCHAR` VARCHAR(20),\n \t\t`CHARACTER` CHARACTER(20),\n `VARYING_CHARACTER` VARYING_CHARACTER(20),\n `NCHAR` NCHAR(23),\n `TEXT` TEXT,\n `CLOB` CLOB,\n `REAL` REAL,\n `DOUBLE` DOUBLE,\n `DOUBLE_PRECISION` DOUBLE PRECISION,\n `FLOAT` FLOAT,\n `DATETIME` DATETIME \n );" + _, err = db.ExecContext(context.Background(), query) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + rows *sql.Rows + want []any + after func() + }{ + { + name: "浮点类型", + rows: func() *sql.Rows { + res, er := db.Exec("INSERT INTO `t1` (`REAL`,`DOUBLE`,`DOUBLE_PRECISION`, `FLOAT`) VALUES (1.0,1.0,1.0,0);") + require.NoError(t, er) + id, _ := res.LastInsertId() + q := "SELECT `REAL`,`DOUBLE`,`DOUBLE_PRECISION`,`FLOAT` FROM `t1` where id=?;" + rows, er := db.QueryContext(context.Background(), q, id) + require.NoError(t, er) + return rows + }(), + want: []any{sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 0}}, + after: func() { + _, er := db.Exec("delete from `t1`") + require.NoError(t, er) + }, + }, + { + name: "整型", + rows: func() *sql.Rows { + res, er := db.Exec("INSERT INTO `t1` (`int`,`integer`,`tinyint`,`smallint`,`MEDIUMINT`,`BIGINT`,`UNSIGNED_BIG_INT`,`INT2`, `INT8`) VALUES (1,1,1,1,1,1,1,1,1);") + require.NoError(t, er) + q := "SELECT `int`,`integer`,`tinyint`,`smallint`,`MEDIUMINT`,`BIGINT`,`UNSIGNED_BIG_INT`,`INT2`,`INT8` FROM `t1` where id=?;" + id, _ := res.LastInsertId() + rows, er := db.QueryContext(context.Background(), q, id) + require.NoError(t, er) + return rows + }(), + want: []any{sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}}, + after: func() { + _, er := db.Exec("delete from `t1`") + require.NoError(t, er) + }, + }, + { + name: "string类型", + rows: func() *sql.Rows { + res, er := db.Exec("INSERT INTO `t1` (`VARCHAR`,`CHARACTER`,`VARYING_CHARACTER`,`NCHAR`,`TEXT`) VALUES ('zwl','zwl','zwl','zwl','zwl');") + require.NoError(t, er) + id, _ := res.LastInsertId() + q := "SELECT `VARCHAR`,`CHARACTER`,`VARYING_CHARACTER`,`NCHAR`,`TEXT`,`CLOB` FROM `t1` where id=?;" + rows, er := db.QueryContext(context.Background(), q, id) + require.NoError(t, er) + return rows + }(), + want: []any{sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: false, String: ""}}, + after: func() { + _, er := db.Exec("delete from `t1`") + require.NoError(t, er) + }, + }, + { + name: "时间类型", + rows: func() *sql.Rows { + res, er := db.Exec("INSERT INTO `t1` (`DATETIME`) VALUES ('2022-01-01 12:00:00');") + require.NoError(t, er) + id, _ := res.LastInsertId() + q := "SELECT `DATETIME` FROM `t1` where id=?;" + rows, er := db.QueryContext(context.Background(), q, id) + require.NoError(t, er) + return rows + }(), + want: []any{sql.NullTime{Valid: true, Time: func() time.Time { + tim, er := time.ParseInLocation("2006-01-02 15:04:05", "2022-01-01 12:00:00", time.Local) + require.NoError(t, er) + return tim + }()}}, + after: func() { + _, er := db.Exec("delete from `t1`") + require.NoError(t, er) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rows := tt.rows + for rows.Next() { + got, er := ScanRows(rows) + assert.Nil(t, er) + assert.Equalf(t, tt.want, got, "ScanRows(%v)", tt.rows) + } + tt.after() + }) + } +} + +func Test_ScanAll(t *testing.T) { + db, err1 := sql.Open("sqlite3", "file:test01.db?cache=shared&mode=memory") + if err1 != nil { + t.Error(err1) + } + defer db.Close() + + query := "DROP TABLE IF EXISTS t1; CREATE TABLE t1 " + + "(id int primary key," + + "`name` VARCHAR(20), " + + "`intro` TEXT, " + + "`create_time` DATETIME);" + _, err2 := db.ExecContext(context.Background(), query) + if err2 != nil { + t.Fatal(err2) + } + + t1, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-01 19:00:01", time.UTC) + t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-04-01 11:00:00", time.UTC) + t3, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-02 09:00:23", time.UTC) + t4, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-04 15:00:00", time.UTC) + + _, err3 := db.Exec("INSERT INTO `t1` (`id`, `name`, `intro`, `create_time`) VALUES " + + "(1, 'zhangsan','这是一段中文介绍', \"2023-02-01 19:00:01\"), " + + "(2, 'lisi','这是一段中文介绍', \"2023-04-01 11:00:00\"), " + + "(3, 'wangwu','this is English introduction', \"2023-02-02 09:00:23\"), " + + "(4, 'zhaoliu','this is English introduction', \"2023-02-04 15:00:00\");") + assert.Nil(t, err3) + + rows, err4 := db.QueryContext(context.Background(), "SELECT * FROM `t1`;") + assert.Nil(t, err4) + + got, err5 := ScanAll(rows) + assert.Nil(t, err5) + + assert.Equal(t, [][]any{ + {sql.NullInt64{Valid: true, Int64: 1}, sql.NullString{Valid: true, String: "zhangsan"}, sql.NullString{Valid: true, String: "这是一段中文介绍"}, sql.NullTime{Valid: true, Time: t1}}, + {sql.NullInt64{Valid: true, Int64: 2}, sql.NullString{Valid: true, String: "lisi"}, sql.NullString{Valid: true, String: "这是一段中文介绍"}, sql.NullTime{Valid: true, Time: t2}}, + {sql.NullInt64{Valid: true, Int64: 3}, sql.NullString{Valid: true, String: "wangwu"}, sql.NullString{Valid: true, String: "this is English introduction"}, sql.NullTime{Valid: true, Time: t3}}, + {sql.NullInt64{Valid: true, Int64: 4}, sql.NullString{Valid: true, String: "zhaoliu"}, sql.NullString{Valid: true, String: "this is English introduction"}, sql.NullTime{Valid: true, Time: t4}}, + }, got) +} From c0c601ce9fb102d70bb126394ed0ba621a51b10b Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Tue, 9 May 2023 13:45:12 +0800 Subject: [PATCH 06/32] =?UTF-8?q?mapx:=20TreeMap=20=E6=B7=BB=E5=8A=A0=20Ke?= =?UTF-8?q?ys=20=E5=92=8C=20Values=20=E6=96=B9=E6=B3=95=20(#181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .CHANGELOG.md | 1 + internal/tree/red_black_tree.go | 8 ++- mapx/hashmap.go | 5 -- mapx/treemap.go | 30 ++++++--- mapx/treemap_test.go | 114 +++++++++++++++++++++++++++----- mapx/types.go | 32 +++++++++ set/set_test.go | 2 +- set/treeset.go | 3 +- 8 files changed, 160 insertions(+), 35 deletions(-) create mode 100644 mapx/types.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 1a646b77..1d5e9577 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,4 +1,5 @@ # 开发中 +- [mapx: TreeMap 添加 Keys 和 Values 方法](https://github.com/ecodeclub/ekit/pull/181) - [pool: 重构TaskPool测试用例](https://github.com/ecodeclub/ekit/pull/178) - [sqlx:ScanRows 和 ScanAll方法](https://github.com/ecodeclub/ekit/pull/180) diff --git a/internal/tree/red_black_tree.go b/internal/tree/red_black_tree.go index 3a38ea63..fb3fcc94 100644 --- a/internal/tree/red_black_tree.go +++ b/internal/tree/red_black_tree.go @@ -85,10 +85,13 @@ func (rb *RBTree[K, V]) Add(key K, value V) error { } // Delete 删除节点 -func (rb *RBTree[K, V]) Delete(key K) { +func (rb *RBTree[K, V]) Delete(key K) (V, bool) { if node := rb.findNode(key); node != nil { rb.deleteNode(node) + return node.value, true } + var v V + return v, false } // Find 查找节点 @@ -184,7 +187,8 @@ func (rb *RBTree[K, V]) addNode(node *rbNode[K, V]) error { // 着色旋转 // case1:当删除节点非空且为黑色时,会违反红黑树任何路径黑节点个数相同的约束,所以需要重新平衡 // case2:当删除红色节点时,不会破坏任何约束,所以不需要平衡 -func (rb *RBTree[K, V]) deleteNode(node *rbNode[K, V]) { +func (rb *RBTree[K, V]) deleteNode(tgt *rbNode[K, V]) { + node := tgt // node左右非空,取后继节点 if node.left != nil && node.right != nil { s := rb.findSuccessor(node) diff --git a/mapx/hashmap.go b/mapx/hashmap.go index eec9fda5..7efdd202 100644 --- a/mapx/hashmap.go +++ b/mapx/hashmap.go @@ -118,11 +118,6 @@ func NewHashMap[T Hashable, ValType any](size int) *HashMap[T, ValType] { } } -type mapi[T any, ValType any] interface { - Put(key T, val ValType) error - Get(key T) (ValType, bool) -} - var _ mapi[Hashable, any] = (*HashMap[Hashable, any])(nil) // Delete 第一个返回值为删除key的值,第二个是hashmap是否真的有这个key diff --git a/mapx/treemap.go b/mapx/treemap.go index 24b0aa79..719d696d 100644 --- a/mapx/treemap.go +++ b/mapx/treemap.go @@ -27,7 +27,7 @@ var ( // TreeMap 是基于红黑树实现的Map type TreeMap[K any, V any] struct { - *tree.RBTree[K, V] + tree *tree.RBTree[K, V] } // NewTreeMapWithMap TreeMap构造方法 @@ -48,7 +48,7 @@ func NewTreeMap[K any, V any](compare ekit.Comparator[K]) (*TreeMap[K, V], error return nil, errTreeMapComparatorIsNull } return &TreeMap[K, V]{ - RBTree: tree.NewRBTree[K, V](compare), + tree: tree.NewRBTree[K, V](compare), }, nil } @@ -63,9 +63,9 @@ func putAll[K comparable, V any](treeMap *TreeMap[K, V], m map[K]V) { // Put 在TreeMap插入指定值 // 需注意如果TreeMap已存在该Key那么原值会被替换 func (treeMap *TreeMap[K, V]) Put(key K, value V) error { - err := treeMap.Add(key, value) + err := treeMap.tree.Add(key, value) if err == tree.ErrRBTreeSameRBNode { - return treeMap.Set(key, value) + return treeMap.tree.Set(key, value) } return nil } @@ -73,13 +73,27 @@ func (treeMap *TreeMap[K, V]) Put(key K, value V) error { // Get 在TreeMap找到指定Key的节点,返回Val // TreeMap未找到指定节点将会返回false func (treeMap *TreeMap[K, V]) Get(key K) (V, bool) { - v, err := treeMap.Find(key) + v, err := treeMap.tree.Find(key) return v, err == nil } -// Remove TreeMap中删除指定key的节点 -func (treeMap *TreeMap[T, V]) Remove(k T) { - treeMap.Delete(k) +// Delete TreeMap中删除指定key的节点 +func (treeMap *TreeMap[T, V]) Delete(k T) (V, bool) { + return treeMap.tree.Delete(k) +} + +// Keys 返回了全部的键 +// 目前我们是按照中序遍历来返回的数据,但是你不能依赖于这个特性 +func (treeMap *TreeMap[T, V]) Keys() []T { + keys, _ := treeMap.tree.KeyValues() + return keys +} + +// Values 返回了全部的值 +// 目前我们是按照中序遍历来返回的数据,但是你不能依赖于这个特性 +func (treeMap *TreeMap[T, V]) Values() []V { + _, vals := treeMap.tree.KeyValues() + return vals } var _ mapi[any, any] = (*TreeMap[any, any])(nil) diff --git a/mapx/treemap_test.go b/mapx/treemap_test.go index f6e9fc86..378000fd 100644 --- a/mapx/treemap_test.go +++ b/mapx/treemap_test.go @@ -18,6 +18,8 @@ import ( "errors" "testing" + "github.com/stretchr/testify/require" + "github.com/ecodeclub/ekit" "github.com/stretchr/testify/assert" ) @@ -277,19 +279,92 @@ func TestTreeMap_Put(t *testing.T) { } } -func TestTreeMap_Remove(t *testing.T) { - var tests = []struct { +func TestTreeMap_Keys(t *testing.T) { + testCases := []struct { name string - m map[int]int - delKey int - wantVal int - wantBool bool + data map[int]int + wantKeys []int + }{ + { + name: "no data", + wantKeys: []int{}, + }, + { + name: "data", + data: map[int]int{ + 1: 11, + 2: 12, + 0: 10, + 3: 13, + 5: 15, + 4: 14, + }, + wantKeys: []int{0, 1, 2, 3, 4, 5}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tm, err := NewTreeMap[int, int](compare()) + require.NoError(t, err) + for k, v := range tc.data { + err = tm.Put(k, v) + require.NoError(t, err) + } + keys := tm.Keys() + assert.Equal(t, tc.wantKeys, keys) + }) + } +} + +func TestTreeMap_Values(t *testing.T) { + testCases := []struct { + name string + data map[int]int + wantValues []int }{ { - name: "empty-TreeMap", - m: map[int]int{}, - delKey: 0, - wantVal: 0, + name: "no data", + wantValues: []int{}, + }, + { + name: "data", + data: map[int]int{ + 1: 11, + 2: 12, + 0: 10, + 3: 13, + 5: 15, + 4: 14, + }, + wantValues: []int{10, 11, 12, 13, 14, 15}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tm, err := NewTreeMap[int, int](compare()) + require.NoError(t, err) + for k, v := range tc.data { + err = tm.Put(k, v) + require.NoError(t, err) + } + vals := tm.Values() + assert.Equal(t, tc.wantValues, vals) + }) + } +} + +func TestTreeMap_Delete(t *testing.T) { + var tests = []struct { + name string + m map[int]int + delKey int + delVal int + deleted bool + }{ + { + name: "empty-TreeMap", + m: map[int]int{}, + delKey: 0, }, { name: "find", @@ -302,7 +377,8 @@ func TestTreeMap_Remove(t *testing.T) { 4: 4, }, delKey: 2, - wantVal: 0, + deleted: true, + delVal: 2, }, { name: "not-find", @@ -314,17 +390,21 @@ func TestTreeMap_Remove(t *testing.T) { 5: 5, 4: 4, }, - delKey: 6, - wantVal: 0, + delKey: 6, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { treeMap, _ := NewTreeMap[int, int](compare()) - treeMap.Remove(tt.delKey) - val, err := treeMap.Get(tt.delKey) - assert.Equal(t, tt.wantBool, err) - assert.Equal(t, tt.wantVal, val) + for k, v := range tt.m { + err := treeMap.Put(k, v) + require.NoError(t, err) + } + delVal, ok := treeMap.Delete(tt.delKey) + assert.Equal(t, tt.deleted, ok) + assert.Equal(t, tt.delVal, delVal) + _, ok = treeMap.Get(tt.delKey) + assert.False(t, ok) }) } } diff --git a/mapx/types.go b/mapx/types.go new file mode 100644 index 00000000..8b43c7aa --- /dev/null +++ b/mapx/types.go @@ -0,0 +1,32 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapx + +type mapi[K any, V any] interface { + Put(key K, val V) error + Get(key K) (V, bool) + // Delete 删除 + // 第一个返回值是被删除的 key 对应的值 + // 第二个返回值是代表是否真的删除了 + Delete(k K) (V, bool) + // Keys 返回所有的键 + // 注意,当你调用多次拿到的结果不一定相等 + // 取决于具体实现 + Keys() []K + // Values 返回所有的值 + // 注意,当你调用多次拿到的结果不一定相等 + // 取决于具体实现 + Values() []V +} diff --git a/set/set_test.go b/set/set_test.go index 9cf80ae2..7abd87a7 100644 --- a/set/set_test.go +++ b/set/set_test.go @@ -35,7 +35,7 @@ func TestSetx_Add(t *testing.T) { }) } -func TestSetx_Remove(t *testing.T) { +func TestSetx_Delete(t *testing.T) { testcases := []struct { name string delVal int diff --git a/set/treeset.go b/set/treeset.go index 788f84c0..1661c48c 100644 --- a/set/treeset.go +++ b/set/treeset.go @@ -48,8 +48,7 @@ func (s *TreeSet[T]) Exist(key T) bool { // Keys 方法返回的元素顺序不固定 func (s *TreeSet[T]) Keys() []T { - keys, _ := s.treeMap.KeyValues() - return keys + return s.treeMap.Keys() } var _ Set[int] = (*TreeSet[int])(nil) From b4540a020e0c962a64b4ccfb95e8d15bd83c5741 Mon Sep 17 00:00:00 2001 From: Longyue Li Date: Thu, 11 May 2023 13:54:06 +0800 Subject: [PATCH 07/32] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BA=A2=E9=BB=91?= =?UTF-8?q?=E6=A0=91=E5=88=A0=E9=99=A4=E8=8A=82=E7=82=B9=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20(#183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复红黑树删除节点问题 --- .CHANGELOG.md | 1 + internal/tree/red_black_tree.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 1d5e9577..ec4b4ab8 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -2,6 +2,7 @@ - [mapx: TreeMap 添加 Keys 和 Values 方法](https://github.com/ecodeclub/ekit/pull/181) - [pool: 重构TaskPool测试用例](https://github.com/ecodeclub/ekit/pull/178) - [sqlx:ScanRows 和 ScanAll方法](https://github.com/ecodeclub/ekit/pull/180) +- [mapx: 修复红黑树删除节点问题](https://github.com/ecodeclub/ekit/pull/183) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/internal/tree/red_black_tree.go b/internal/tree/red_black_tree.go index fb3fcc94..cec0eac0 100644 --- a/internal/tree/red_black_tree.go +++ b/internal/tree/red_black_tree.go @@ -87,8 +87,9 @@ func (rb *RBTree[K, V]) Add(key K, value V) error { // Delete 删除节点 func (rb *RBTree[K, V]) Delete(key K) (V, bool) { if node := rb.findNode(key); node != nil { + value := node.value rb.deleteNode(node) - return node.value, true + return value, true } var v V return v, false From a1207754a4a5802f398fae5a04d0b6fe272aeb16 Mon Sep 17 00:00:00 2001 From: Longyue Li Date: Sat, 13 May 2023 22:02:33 +0800 Subject: [PATCH 08/32] =?UTF-8?q?sqlx:=20=E7=94=A8sqlx/Scanner=E6=8A=BD?= =?UTF-8?q?=E8=B1=A1=E6=9B=BF=E4=BB=A3=E7=8E=B0=E6=9C=89ScanRows=E5=8F=8AS?= =?UTF-8?q?canAll=20(#182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Longyue Li --- .CHANGELOG.md | 1 + go.mod | 1 + go.sum | 2 + sqlx/scan.go | 58 ---------- sqlx/scan_test.go | 174 ---------------------------- sqlx/scanner.go | 100 ++++++++++++++++ sqlx/scanner_test.go | 263 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 367 insertions(+), 232 deletions(-) delete mode 100644 sqlx/scan.go delete mode 100644 sqlx/scan_test.go create mode 100644 sqlx/scanner.go create mode 100644 sqlx/scanner_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index ec4b4ab8..fa4b0d6b 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -3,6 +3,7 @@ - [pool: 重构TaskPool测试用例](https://github.com/ecodeclub/ekit/pull/178) - [sqlx:ScanRows 和 ScanAll方法](https://github.com/ecodeclub/ekit/pull/180) - [mapx: 修复红黑树删除节点问题](https://github.com/ecodeclub/ekit/pull/183) +- [sqlx: 构建Scanner抽象替代现有ScanRows及ScanAll](https://github.com/ecodeclub/ekit/pull/182) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/go.mod b/go.mod index 4b69fc2b..80ea97db 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ecodeclub/ekit go 1.20 require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/mattn/go-sqlite3 v1.14.15 github.com/stretchr/testify v1.8.1 golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index b786e861..92a1b932 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/sqlx/scan.go b/sqlx/scan.go deleted file mode 100644 index 3b0b1baf..00000000 --- a/sqlx/scan.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2021 ecodeclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlx - -import ( - "database/sql" - "reflect" -) - -func ScanRows(rows *sql.Rows) ([]any, error) { - colsInfo, err := rows.ColumnTypes() - if err != nil { - return nil, err - } - colsData := make([]any, 0, len(colsInfo)) - for _, colInfo := range colsInfo { - typ := colInfo.ScanType() - // 保险起见,循环的去除指针 - for typ.Kind() == reflect.Pointer { - typ = typ.Elem() - } - newData := reflect.New(typ).Interface() - colsData = append(colsData, newData) - } - err = rows.Scan(colsData...) - if err != nil { - return nil, err - } - // 去掉reflect.New的指针 - for i := 0; i < len(colsData); i++ { - colsData[i] = reflect.ValueOf(colsData[i]).Elem().Interface() - } - return colsData, nil -} - -func ScanAll(rows *sql.Rows) ([][]any, error) { - res := make([][]any, 0, 32) - for rows.Next() { - cols, err := ScanRows(rows) - if err != nil { - return nil, err - } - res = append(res, cols) - } - return res, nil -} diff --git a/sqlx/scan_test.go b/sqlx/scan_test.go deleted file mode 100644 index 7622ded8..00000000 --- a/sqlx/scan_test.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2021 ecodeclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlx - -import ( - "context" - "database/sql" - "testing" - "time" - - _ "github.com/mattn/go-sqlite3" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestScanRows(t *testing.T) { - db, err := sql.Open("sqlite3", "file:test01.db?cache=shared&mode=memory") - if err != nil { - t.Error(err) - } - defer db.Close() - - query := "DROP TABLE IF EXISTS t1; CREATE TABLE t1 (\n id int primary key,\n `int` int,\n `integer` integer,\n `tinyint` TINYINT,\n `smallint` smallint,\n `MEDIUMINT` MEDIUMINT,\n `BIGINT` BIGINT,\n `UNSIGNED_BIG_INT` UNSIGNED BIG INT,\n `INT2` INT2,\n `INT8` INT8,\n `VARCHAR` VARCHAR(20),\n \t\t`CHARACTER` CHARACTER(20),\n `VARYING_CHARACTER` VARYING_CHARACTER(20),\n `NCHAR` NCHAR(23),\n `TEXT` TEXT,\n `CLOB` CLOB,\n `REAL` REAL,\n `DOUBLE` DOUBLE,\n `DOUBLE_PRECISION` DOUBLE PRECISION,\n `FLOAT` FLOAT,\n `DATETIME` DATETIME \n );" - _, err = db.ExecContext(context.Background(), query) - if err != nil { - t.Fatal(err) - } - - tests := []struct { - name string - rows *sql.Rows - want []any - after func() - }{ - { - name: "浮点类型", - rows: func() *sql.Rows { - res, er := db.Exec("INSERT INTO `t1` (`REAL`,`DOUBLE`,`DOUBLE_PRECISION`, `FLOAT`) VALUES (1.0,1.0,1.0,0);") - require.NoError(t, er) - id, _ := res.LastInsertId() - q := "SELECT `REAL`,`DOUBLE`,`DOUBLE_PRECISION`,`FLOAT` FROM `t1` where id=?;" - rows, er := db.QueryContext(context.Background(), q, id) - require.NoError(t, er) - return rows - }(), - want: []any{sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 0}}, - after: func() { - _, er := db.Exec("delete from `t1`") - require.NoError(t, er) - }, - }, - { - name: "整型", - rows: func() *sql.Rows { - res, er := db.Exec("INSERT INTO `t1` (`int`,`integer`,`tinyint`,`smallint`,`MEDIUMINT`,`BIGINT`,`UNSIGNED_BIG_INT`,`INT2`, `INT8`) VALUES (1,1,1,1,1,1,1,1,1);") - require.NoError(t, er) - q := "SELECT `int`,`integer`,`tinyint`,`smallint`,`MEDIUMINT`,`BIGINT`,`UNSIGNED_BIG_INT`,`INT2`,`INT8` FROM `t1` where id=?;" - id, _ := res.LastInsertId() - rows, er := db.QueryContext(context.Background(), q, id) - require.NoError(t, er) - return rows - }(), - want: []any{sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}}, - after: func() { - _, er := db.Exec("delete from `t1`") - require.NoError(t, er) - }, - }, - { - name: "string类型", - rows: func() *sql.Rows { - res, er := db.Exec("INSERT INTO `t1` (`VARCHAR`,`CHARACTER`,`VARYING_CHARACTER`,`NCHAR`,`TEXT`) VALUES ('zwl','zwl','zwl','zwl','zwl');") - require.NoError(t, er) - id, _ := res.LastInsertId() - q := "SELECT `VARCHAR`,`CHARACTER`,`VARYING_CHARACTER`,`NCHAR`,`TEXT`,`CLOB` FROM `t1` where id=?;" - rows, er := db.QueryContext(context.Background(), q, id) - require.NoError(t, er) - return rows - }(), - want: []any{sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: false, String: ""}}, - after: func() { - _, er := db.Exec("delete from `t1`") - require.NoError(t, er) - }, - }, - { - name: "时间类型", - rows: func() *sql.Rows { - res, er := db.Exec("INSERT INTO `t1` (`DATETIME`) VALUES ('2022-01-01 12:00:00');") - require.NoError(t, er) - id, _ := res.LastInsertId() - q := "SELECT `DATETIME` FROM `t1` where id=?;" - rows, er := db.QueryContext(context.Background(), q, id) - require.NoError(t, er) - return rows - }(), - want: []any{sql.NullTime{Valid: true, Time: func() time.Time { - tim, er := time.ParseInLocation("2006-01-02 15:04:05", "2022-01-01 12:00:00", time.Local) - require.NoError(t, er) - return tim - }()}}, - after: func() { - _, er := db.Exec("delete from `t1`") - require.NoError(t, er) - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - rows := tt.rows - for rows.Next() { - got, er := ScanRows(rows) - assert.Nil(t, er) - assert.Equalf(t, tt.want, got, "ScanRows(%v)", tt.rows) - } - tt.after() - }) - } -} - -func Test_ScanAll(t *testing.T) { - db, err1 := sql.Open("sqlite3", "file:test01.db?cache=shared&mode=memory") - if err1 != nil { - t.Error(err1) - } - defer db.Close() - - query := "DROP TABLE IF EXISTS t1; CREATE TABLE t1 " + - "(id int primary key," + - "`name` VARCHAR(20), " + - "`intro` TEXT, " + - "`create_time` DATETIME);" - _, err2 := db.ExecContext(context.Background(), query) - if err2 != nil { - t.Fatal(err2) - } - - t1, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-01 19:00:01", time.UTC) - t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-04-01 11:00:00", time.UTC) - t3, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-02 09:00:23", time.UTC) - t4, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-04 15:00:00", time.UTC) - - _, err3 := db.Exec("INSERT INTO `t1` (`id`, `name`, `intro`, `create_time`) VALUES " + - "(1, 'zhangsan','这是一段中文介绍', \"2023-02-01 19:00:01\"), " + - "(2, 'lisi','这是一段中文介绍', \"2023-04-01 11:00:00\"), " + - "(3, 'wangwu','this is English introduction', \"2023-02-02 09:00:23\"), " + - "(4, 'zhaoliu','this is English introduction', \"2023-02-04 15:00:00\");") - assert.Nil(t, err3) - - rows, err4 := db.QueryContext(context.Background(), "SELECT * FROM `t1`;") - assert.Nil(t, err4) - - got, err5 := ScanAll(rows) - assert.Nil(t, err5) - - assert.Equal(t, [][]any{ - {sql.NullInt64{Valid: true, Int64: 1}, sql.NullString{Valid: true, String: "zhangsan"}, sql.NullString{Valid: true, String: "这是一段中文介绍"}, sql.NullTime{Valid: true, Time: t1}}, - {sql.NullInt64{Valid: true, Int64: 2}, sql.NullString{Valid: true, String: "lisi"}, sql.NullString{Valid: true, String: "这是一段中文介绍"}, sql.NullTime{Valid: true, Time: t2}}, - {sql.NullInt64{Valid: true, Int64: 3}, sql.NullString{Valid: true, String: "wangwu"}, sql.NullString{Valid: true, String: "this is English introduction"}, sql.NullTime{Valid: true, Time: t3}}, - {sql.NullInt64{Valid: true, Int64: 4}, sql.NullString{Valid: true, String: "zhaoliu"}, sql.NullString{Valid: true, String: "this is English introduction"}, sql.NullTime{Valid: true, Time: t4}}, - }, got) -} diff --git a/sqlx/scanner.go b/sqlx/scanner.go new file mode 100644 index 00000000..d0c1d21d --- /dev/null +++ b/sqlx/scanner.go @@ -0,0 +1,100 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlx + +import ( + "database/sql" + "errors" + "fmt" + "reflect" +) + +var ( + ErrNoMoreRows = errors.New("ekit: 已读取完") + errInvalidArgument = errors.New("ekit: 参数非法") + _ Scanner = &sqlRowsScanner{} +) + +// Scanner 用于简化sql.Rows包中的Scan操作 +// Scanner 不会关闭sql.Rows,用户需要对此负责 +type Scanner interface { + Scan() (values []any, err error) + ScanAll() (allValues [][]any, err error) +} + +type sqlRowsScanner struct { + sqlRows *sql.Rows + columnValuePointers []any +} + +// NewSQLRowsScanner 返回一个Scanner +func NewSQLRowsScanner(r *sql.Rows) (Scanner, error) { + if r == nil { + return nil, fmt.Errorf("%w *sql.Rows不能为nil", errInvalidArgument) + } + columnTypes, err := r.ColumnTypes() + if err != nil || len(columnTypes) < 1 { + return nil, fmt.Errorf("%w 无法获取*sql.Rows列类型信息: %v", errInvalidArgument, err) + } + columnValuePointers := make([]any, len(columnTypes)) + for i, columnType := range columnTypes { + typ := columnType.ScanType() + for typ.Kind() == reflect.Pointer { + typ = typ.Elem() + } + columnValuePointers[i] = reflect.New(typ).Interface() + } + return &sqlRowsScanner{sqlRows: r, columnValuePointers: columnValuePointers}, nil +} + +// Scan 返回一行 +func (s *sqlRowsScanner) Scan() ([]any, error) { + if !s.sqlRows.Next() { + if err := s.sqlRows.Err(); err != nil { + return nil, err + } + + return nil, fmt.Errorf("%w", ErrNoMoreRows) + } + err := s.sqlRows.Scan(s.columnValuePointers...) + if err != nil { + return nil, err + } + return s.columnValues(), nil +} + +func (s *sqlRowsScanner) columnValues() []any { + values := make([]any, len(s.columnValuePointers)) + for i := 0; i < len(s.columnValuePointers); i++ { + values[i] = reflect.ValueOf(s.columnValuePointers[i]).Elem().Interface() + } + return values +} + +// ScanAll 返回所有行 +func (s *sqlRowsScanner) ScanAll() ([][]any, error) { + all := make([][]any, 0, 32) + for { + columnValues, err := s.Scan() + if err != nil { + if errors.Is(err, ErrNoMoreRows) { + break + } + return nil, err + } + all = append(all, columnValues) + } + return all, nil +} diff --git a/sqlx/scanner_test.go b/sqlx/scanner_test.go new file mode 100644 index 00000000..be66eda2 --- /dev/null +++ b/sqlx/scanner_test.go @@ -0,0 +1,263 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlx + +import ( + "context" + "database/sql" + "errors" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSqlRowsScanner_New(t *testing.T) { + t.Parallel() + t.Run("当*sql.Rows为nil时,应该报错", func(t *testing.T) { + t.Parallel() + _, err := NewSQLRowsScanner(nil) + require.ErrorIs(t, err, errInvalidArgument) + }) + t.Run("当无法获取*sql.Rows列类型信息时,应该报错", func(t *testing.T) { + t.Parallel() + t.Run("*sql.Rows已关闭", func(t *testing.T) { + t.Parallel() + db, err := sql.Open("sqlite3", ":memory:") + require.NoError(t, err) + defer db.Close() + rows, err := db.QueryContext(context.Background(), "") + require.NoError(t, err) + require.NoError(t, rows.Close()) + + _, err = NewSQLRowsScanner(rows) + assert.Error(t, err) + }) + t.Run("*sql.Rows无列类型信息", func(t *testing.T) { + t.Parallel() + db, err := sql.Open("sqlite3", ":memory:") + require.NoError(t, err) + defer db.Close() + rows, err := db.QueryContext(context.Background(), "") + require.NoError(t, err) + + _, err = NewSQLRowsScanner(rows) + assert.ErrorIs(t, err, errInvalidArgument) + }) + }) +} + +func TestSqlRowsScanner_Scan(t *testing.T) { + db, err := sql.Open("sqlite3", "file:test01.db?cache=shared&mode=memory") + require.NoError(t, err) + defer db.Close() + + query := "DROP TABLE IF EXISTS t1; CREATE TABLE t1 (\n id int primary key,\n `int` int,\n `integer` integer,\n `tinyint` TINYINT,\n `smallint` smallint,\n `MEDIUMINT` MEDIUMINT,\n `BIGINT` BIGINT,\n `UNSIGNED_BIG_INT` UNSIGNED BIG INT,\n `INT2` INT2,\n `INT8` INT8,\n `VARCHAR` VARCHAR(20),\n \t\t`CHARACTER` CHARACTER(20),\n `VARYING_CHARACTER` VARYING_CHARACTER(20),\n `NCHAR` NCHAR(23),\n `TEXT` TEXT,\n `CLOB` CLOB,\n `REAL` REAL,\n `DOUBLE` DOUBLE,\n `DOUBLE_PRECISION` DOUBLE PRECISION,\n `FLOAT` FLOAT,\n `DATETIME` DATETIME \n );" + _, err = db.ExecContext(context.Background(), query) + require.NoError(t, err) + + tests := []struct { + name string + rows *sql.Rows + want []any + cleanup func() + }{ + { + name: "浮点类型", + rows: func() *sql.Rows { + res, er := db.Exec("INSERT INTO `t1` (`REAL`,`DOUBLE`,`DOUBLE_PRECISION`, `FLOAT`) VALUES (1.0,1.0,1.0,0);") + require.NoError(t, er) + id, _ := res.LastInsertId() + q := "SELECT `REAL`,`DOUBLE`,`DOUBLE_PRECISION`,`FLOAT` FROM `t1` WHERE id=?;" + rows, er := db.QueryContext(context.Background(), q, id) + require.NoError(t, er) + return rows + }(), + want: []any{sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 1.0}, sql.NullFloat64{Valid: true, Float64: 0}}, + cleanup: func() { + _, er := db.Exec("DELETE FROM `t1`") + require.NoError(t, er) + }, + }, + { + name: "整型", + rows: func() *sql.Rows { + res, er := db.Exec("INSERT INTO `t1` (`int`,`integer`,`tinyint`,`smallint`,`MEDIUMINT`,`BIGINT`,`UNSIGNED_BIG_INT`,`INT2`, `INT8`) VALUES (1,1,1,1,1,1,1,1,1);") + require.NoError(t, er) + q := "SELECT `int`,`integer`,`tinyint`,`smallint`,`MEDIUMINT`,`BIGINT`,`UNSIGNED_BIG_INT`,`INT2`,`INT8` FROM `t1` WHERE id=?;" + id, _ := res.LastInsertId() + rows, er := db.QueryContext(context.Background(), q, id) + require.NoError(t, er) + return rows + }(), + want: []any{sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}, sql.NullInt64{Valid: true, Int64: 1}}, + cleanup: func() { + _, er := db.Exec("DELETE FROM `t1`") + require.NoError(t, er) + }, + }, + { + name: "string类型", + rows: func() *sql.Rows { + res, er := db.Exec("INSERT INTO `t1` (`VARCHAR`,`CHARACTER`,`VARYING_CHARACTER`,`NCHAR`,`TEXT`) VALUES ('zwl','zwl','zwl','zwl','zwl');") + require.NoError(t, er) + id, _ := res.LastInsertId() + q := "SELECT `VARCHAR`,`CHARACTER`,`VARYING_CHARACTER`,`NCHAR`,`TEXT`,`CLOB` FROM `t1` WHERE id=?;" + rows, er := db.QueryContext(context.Background(), q, id) + require.NoError(t, er) + return rows + }(), + want: []any{sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: true, String: "zwl"}, sql.NullString{Valid: false, String: ""}}, + cleanup: func() { + _, er := db.Exec("DELETE FROM `t1`") + require.NoError(t, er) + }, + }, + { + name: "时间类型", + rows: func() *sql.Rows { + res, er := db.Exec("INSERT INTO `t1` (`DATETIME`) VALUES ('2022-01-01 12:00:00');") + require.NoError(t, er) + id, _ := res.LastInsertId() + q := "SELECT `DATETIME` FROM `t1` WHERE id=?;" + rows, er := db.QueryContext(context.Background(), q, id) + require.NoError(t, er) + return rows + }(), + want: []any{sql.NullTime{Valid: true, Time: func() time.Time { + tim, er := time.ParseInLocation("2006-01-02 15:04:05", "2022-01-01 12:00:00", time.Local) + require.NoError(t, er) + return tim + }()}}, + cleanup: func() { + _, er := db.Exec("DELETE FROM `t1`") + require.NoError(t, er) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, err := NewSQLRowsScanner(tt.rows) + require.NoError(t, err) + for { + got, err := s.Scan() + if err != nil && errors.Is(err, ErrNoMoreRows) { + break + } + assert.NoError(t, err) + assert.Equalf(t, tt.want, got, "ScanRows(%v)", tt.rows) + } + tt.cleanup() + }) + } + + t.Run("迭代期间sql.Rows发生错误,Scan应该报错", func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + expectedErr := errors.New("iteration error") + mock.ExpectQuery("SELECT").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). + AddRow(1, "John"). + AddRow(2, "Jane").RowError(1, expectedErr)) + + rows, err := db.Query("SELECT id, name FROM users") + require.NoError(t, err) + defer rows.Close() + + s, err := NewSQLRowsScanner(rows) + require.NoError(t, err) + + values, err := s.Scan() + assert.NoError(t, err) + assert.Equal(t, []any{int64(1), "John"}, values) + + _, err = s.Scan() + assert.Equal(t, expectedErr, err) + }) +} + +func TestSqlRowsScanner_ScanAll(t *testing.T) { + t.Parallel() + t.Run("迭代期间sql.Rows没有错误,ScanAll正常结束", func(t *testing.T) { + t.Parallel() + db, err := sql.Open("sqlite3", "file:test01.db?cache=shared&mode=memory") + require.NoError(t, err) + defer db.Close() + + query := "DROP TABLE IF EXISTS t1; CREATE TABLE t1 " + + "(id int primary key," + + "`name` VARCHAR(20), " + + "`intro` TEXT, " + + "`create_time` DATETIME);" + _, err = db.ExecContext(context.Background(), query) + require.NoError(t, err) + + t1, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-01 19:00:01", time.UTC) + t2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-04-01 11:00:00", time.UTC) + t3, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-02 09:00:23", time.UTC) + t4, _ := time.ParseInLocation("2006-01-02 15:04:05", "2023-02-04 15:00:00", time.UTC) + + _, err = db.Exec("INSERT INTO `t1` (`id`, `name`, `intro`, `create_time`) VALUES " + + "(1, 'zhangsan','这是一段中文介绍', \"2023-02-01 19:00:01\"), " + + "(2, 'lisi','这是一段中文介绍', \"2023-04-01 11:00:00\"), " + + "(3, 'wangwu','this is English introduction', \"2023-02-02 09:00:23\"), " + + "(4, 'zhaoliu','this is English introduction', \"2023-02-04 15:00:00\");") + require.NoError(t, err) + + expected := [][]any{ + {sql.NullInt64{Valid: true, Int64: 1}, sql.NullString{Valid: true, String: "zhangsan"}, sql.NullString{Valid: true, String: "这是一段中文介绍"}, sql.NullTime{Valid: true, Time: t1}}, + {sql.NullInt64{Valid: true, Int64: 2}, sql.NullString{Valid: true, String: "lisi"}, sql.NullString{Valid: true, String: "这是一段中文介绍"}, sql.NullTime{Valid: true, Time: t2}}, + {sql.NullInt64{Valid: true, Int64: 3}, sql.NullString{Valid: true, String: "wangwu"}, sql.NullString{Valid: true, String: "this is English introduction"}, sql.NullTime{Valid: true, Time: t3}}, + {sql.NullInt64{Valid: true, Int64: 4}, sql.NullString{Valid: true, String: "zhaoliu"}, sql.NullString{Valid: true, String: "this is English introduction"}, sql.NullTime{Valid: true, Time: t4}}, + } + + rows, err := db.QueryContext(context.Background(), "SELECT * FROM `t1`;") + require.NoError(t, err) + defer rows.Close() + + s, err := NewSQLRowsScanner(rows) + require.NoError(t, err) + + actual, err := s.ScanAll() + assert.NoError(t, err) + assert.Equal(t, expected, actual) + }) + t.Run("迭代期间sql.Rows发生错误,ScanAll应该报错", func(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + expectedErr := errors.New("iteration error") + + mock.ExpectQuery("SELECT").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). + AddRow(1, "John"). + AddRow(2, "Jane").RowError(1, expectedErr)) + + rows, err := db.Query("SELECT id, name FROM users") + require.NoError(t, err) + defer rows.Close() + + s, err := NewSQLRowsScanner(rows) + require.NoError(t, err) + + _, err = s.ScanAll() + assert.Equal(t, expectedErr, err) + }) +} From 59a4fd1cc99e2ffb657097c753b5d3249fbf7d11 Mon Sep 17 00:00:00 2001 From: Longyue Li Date: Sun, 21 May 2023 13:37:34 +0800 Subject: [PATCH 09/32] =?UTF-8?q?refacor/Taskpool:=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=86=85=E5=A4=96=E4=B8=AD=E6=96=AD=E4=BF=A1=E5=8F=B7+?= =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug=20(#184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refacor: 统一内外中断信号 * 修改.CHANGELOG.md * 1. 重构allowToCreateGoroutine方法,使用直接的bool表达式表明意图 2. 抽取numOfGoThatCanBeCreate方法,增强可读性 3. 对goroutine方法中注释进行整理,并抽取noTasksToExecute变量——增强可读性表明意图 * 1. 将interruptCancel重命名为interruptCtxCancel 2. 将gorutine方法中的time.NewTimer(1)改回为time.NewTimer(0)——延长时间不起作用 3. 修复隐秘bug --- .CHANGELOG.md | 1 + pool/task_pool.go | 122 +++++++++++++++++----------------------------- 2 files changed, 45 insertions(+), 78 deletions(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index fa4b0d6b..67fa4874 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -4,6 +4,7 @@ - [sqlx:ScanRows 和 ScanAll方法](https://github.com/ecodeclub/ekit/pull/180) - [mapx: 修复红黑树删除节点问题](https://github.com/ecodeclub/ekit/pull/183) - [sqlx: 构建Scanner抽象替代现有ScanRows及ScanAll](https://github.com/ecodeclub/ekit/pull/182) +- [pool: 重构TaskPool](https://github.com/ecodeclub/ekit/pull/184) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/pool/task_pool.go b/pool/task_pool.go index 8b0a65bb..78b34dfa 100644 --- a/pool/task_pool.go +++ b/pool/task_pool.go @@ -136,14 +136,9 @@ type OnDemandBlockTaskPool struct { // 协程id方便调试程序 id int32 - // 外部信号 - //shutdownDone chan struct{} - shutdownCtx context.Context - shutdownCancel context.CancelFunc - - // 内部中断信号 - shutdownNowCtx context.Context - shutdownNowCancel context.CancelFunc + // 中断信号 + interruptCtx context.Context + interruptCtxCancel context.CancelFunc } // NewOnDemandBlockTaskPool 创建一个新的 OnDemandBlockTaskPool @@ -165,8 +160,7 @@ func NewOnDemandBlockTaskPool(initGo int, queueSize int, opts ...option.Option[O maxIdleTime: defaultMaxIdleTime, } ctx := context.Background() - b.shutdownCtx, b.shutdownCancel = context.WithCancel(ctx) - b.shutdownNowCtx, b.shutdownNowCancel = context.WithCancel(ctx) + b.interruptCtx, b.interruptCtxCancel = context.WithCancel(ctx) atomic.StoreInt32(&b.state, stateCreated) option.Apply(b, opts...) @@ -275,25 +269,8 @@ func (b *OnDemandBlockTaskPool) trySubmit(ctx context.Context, task Task, state func (b *OnDemandBlockTaskPool) allowToCreateGoroutine() bool { b.mutex.RLock() defer b.mutex.RUnlock() - - if b.totalGo == b.maxGo { - return false - } - - // 这个判断可能太苛刻了,经常导致开协程失败,先注释掉 - // allGoShouldBeBusy := atomic.LoadInt32(&b.numGoRunningTasks) == b.totalGo - // if !allGoShouldBeBusy { - // return false - // } - rate := float64(len(b.queue)) / float64(cap(b.queue)) - if rate == 0 || rate < b.queueBacklogRate { - // log.Println("rate == 0", rate == 0, "rate", rate, " < ", b.queueBacklogRate) - return false - } - - // b.totalGo < b.maxGo && rate != 0 && rate >= b.queueBacklogRate - return true + return (b.totalGo < b.maxGo) && (rate != 0 && rate >= b.queueBacklogRate) } // Start 开始调度任务执行 @@ -316,17 +293,7 @@ func (b *OnDemandBlockTaskPool) Start() error { if atomic.CompareAndSwapInt32(&b.state, stateCreated, stateLocked) { - n := b.initGo - - allowGo := b.maxGo - b.initGo - needGo := int32(len(b.queue)) - b.initGo - if needGo > 0 { - if needGo <= allowGo { - n += needGo - } else { - n += allowGo - } - } + n := b.numOfGoThatCanBeCreate() b.increaseTotalGo(n) for i := int32(0); i < n; i++ { @@ -338,6 +305,20 @@ func (b *OnDemandBlockTaskPool) Start() error { } } +func (b *OnDemandBlockTaskPool) numOfGoThatCanBeCreate() int32 { + n := b.initGo + allowGo := b.maxGo - b.initGo + needGo := int32(len(b.queue)) - b.initGo + if needGo > 0 { + if needGo <= allowGo { + n += needGo + } else { + n += allowGo + } + } + return n +} + func (b *OnDemandBlockTaskPool) goroutine(id int) { // 刚启动的协程除非恰巧赶上Shutdown/ShutdownNow被调用,否则应该至少执行一个task @@ -349,7 +330,7 @@ func (b *OnDemandBlockTaskPool) goroutine(id int) { for { // log.Println("id", id, "working for loop") select { - case <-b.shutdownNowCtx.Done(): + case <-b.interruptCtx.Done(): // log.Printf("id %d shutdownNow, timeoutGroup.Size=%d left\n", id, b.timeoutGroup.size()) b.decreaseTotalGo(1) return @@ -372,50 +353,42 @@ func (b *OnDemandBlockTaskPool) goroutine(id int) { } // log.Println("id", id, "out timeoutGroup") } - atomic.AddInt32(&b.numGoRunningTasks, 1) if !ok { - // b.numGoRunningTasks > 1表示虽然当前协程监听到了b.queue关闭但还有其他协程运行task,当前协程自己退出就好 - // b.numGoRunningTasks == 1表示只有当前协程"运行task"中,其他协程在一定在"拿到b.queue到已关闭",这一信号的路上 - // 绝不会处于运行task中 - if atomic.LoadInt32(&b.state) == stateClosing && atomic.CompareAndSwapInt32(&b.numGoRunningTasks, 1, 0) { - // 在b.queue关闭后,第一个检测到全部task已经自然结束的协程 - // 状态迁移 + b.decreaseTotalGo(1) + if b.numOfGo() == 0 { + // 因调用Shutdown方法导致的协程退出,最后一个退出的协程负责状态迁移及显示通知外部调用者 if atomic.CompareAndSwapInt32(&b.state, stateClosing, stateStopped) { - // 显示通知外部调用者 - b.shutdownCancel() + b.interruptCtxCancel() } - - b.decreaseTotalGo(1) - return } - - // 有其他协程运行task中,自己退出就好。 - atomic.AddInt32(&b.numGoRunningTasks, -1) - b.decreaseTotalGo(1) return } + // todo handle error - _ = task.Run(b.shutdownNowCtx) + atomic.AddInt32(&b.numGoRunningTasks, 1) + _ = task.Run(b.interruptCtx) atomic.AddInt32(&b.numGoRunningTasks, -1) b.mutex.Lock() // log.Println("id", id, "totalGo-mem", b.totalGo-b.timeoutGroup.size(), "totalGo", b.totalGo, "mem", b.timeoutGroup.size()) - if b.coreGo < b.totalGo && (len(b.queue) == 0 || int32(len(b.queue)) < b.totalGo) { - // 协程在(coreGo,maxGo]区间 - // 如果没有任务可以执行,或者被判定为可能抢不到任务的协程直接退出 - // 注意:一定要在此处减1才能让此刻等待在mutex上的其他协程被正确地分区 + noTasksToExecute := len(b.queue) == 0 || int32(len(b.queue)) < b.totalGo + if b.coreGo < b.totalGo && b.totalGo <= b.maxGo && noTasksToExecute { + // 当前协程属于(coreGo,maxGo]区间,发现没有任务可以执行故直接退出 + // 注意:一定要在此处减1才能让此刻等待在mutex上的其他协程被正确地划分区间 b.totalGo-- // log.Println("id", id, "exits....") b.mutex.Unlock() return } - if b.initGo < b.totalGo-b.timeoutGroup.size() /* && len(b.queue) == 0 */ { + if b.initGo < b.totalGo-b.timeoutGroup.size() { // log.Println("id", id, "initGo", b.initGo, "totalGo-mem", b.totalGo-b.timeoutGroup.size(), "totalGo", b.totalGo) - // 协程在(initGo,coreGo]区间,如果没有任务可以执行,重置计时器 - // 当len(b.queue) != 0时,即便协程属于(coreGo,maxGo]区间,也应该给它一个定时器兜底。 - // 因为现在看队列中有任务,等真去拿的时候可能恰好没任务,如果不给它一个定时器兜底此时就会出现当前协程总数长时间大于始协程数(initGo)的情况。 - // 直到队列再次有任务时才可能将当前总协程数准确无误地降至初始协程数,因此注释掉len(b.queue) == 0判断条件 + // 根据需求: + // 1. 如果当前协程属于(initGo,coreGo]区间,需要为其分配一个超时器。 + // - 当前协程在超时退出前(最大空闲时间内)尝试拿任务,拿到则继续执行,没拿到则超时退出。 + // 2. 如果当前协程属于(coreGo, maxGo]区间,且有任务可执行,也需要为其分配一个超时器兜底。 + // - 因为此时看队列中有任务,等真去拿的时候可能恰好没任务 + // - 这会导致当前协程总数(totalGo)长时间大于始协程数(initGo)直到队列再次有任务时才可能将当前总协程数准确地降至初始协程数 idleTimer = time.NewTimer(b.maxIdleTime) b.timeoutGroup.add(id) // log.Println("id", id, "add timeoutGroup", "size", b.timeoutGroup.size()) @@ -465,7 +438,7 @@ func (b *OnDemandBlockTaskPool) Shutdown() (<-chan struct{}, error) { // 先关闭等待队列不再允许提交 // 同时工作协程能够通过判断b.queue是否被关闭来终止获取任务循环 close(b.queue) - return b.shutdownCtx.Done(), nil + return b.interruptCtx.Done(), nil } } @@ -495,7 +468,7 @@ func (b *OnDemandBlockTaskPool) ShutdownNow() ([]Task, error) { close(b.queue) // 发送中断信号,中断工作协程获取任务循环 - b.shutdownNowCancel() + b.interruptCtxCancel() // 清空队列并保存 tasks := make([]Task, 0, len(b.queue)) @@ -530,11 +503,8 @@ func (b *OnDemandBlockTaskPool) States(ctx context.Context, interval time.Durati if ctx.Err() != nil { return nil, ctx.Err() } - if b.shutdownNowCtx.Err() != nil { - return nil, b.shutdownNowCtx.Err() - } - if b.shutdownCtx.Err() != nil { - return nil, b.shutdownCtx.Err() + if b.interruptCtx.Err() != nil { + return nil, b.interruptCtx.Err() } statsChan := make(chan State) @@ -549,11 +519,7 @@ func (b *OnDemandBlockTaskPool) States(ctx context.Context, interval time.Durati b.sendState(statsChan, time.Now().UnixNano()) close(statsChan) return - case <-b.shutdownNowCtx.Done(): - b.sendState(statsChan, time.Now().UnixNano()) - close(statsChan) - return - case <-b.shutdownCtx.Done(): + case <-b.interruptCtx.Done(): b.sendState(statsChan, time.Now().UnixNano()) close(statsChan) return From e101f7ffe101339b64feb3fc55914b7639480a9b Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Sun, 21 May 2023 22:29:51 +0800 Subject: [PATCH 10/32] =?UTF-8?q?mapx:=20=E4=BF=AE=E6=AD=A3=20HashMap=20?= =?UTF-8?q?=E4=B8=AD=E4=BD=BF=E7=94=A8=E6=B3=9B=E5=9E=8B=E4=B8=8D=E5=BD=93?= =?UTF-8?q?=E7=9A=84=E5=9C=B0=E6=96=B9=20(#186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 邓明 --- .CHANGELOG.md | 1 + mapx/hashmap.go | 8 ++++---- mapx/hashmap_test.go | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 67fa4874..867ab65d 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,5 +1,6 @@ # 开发中 - [mapx: TreeMap 添加 Keys 和 Values 方法](https://github.com/ecodeclub/ekit/pull/181) +- [mapx: 修正 HashMap 中使用泛型不当的地方](https://github.com/ecodeclub/ekit/pull/186) - [pool: 重构TaskPool测试用例](https://github.com/ecodeclub/ekit/pull/178) - [sqlx:ScanRows 和 ScanAll方法](https://github.com/ecodeclub/ekit/pull/180) - [mapx: 修复红黑树删除节点问题](https://github.com/ecodeclub/ekit/pull/183) diff --git a/mapx/hashmap.go b/mapx/hashmap.go index 7efdd202..f2a7a6a5 100644 --- a/mapx/hashmap.go +++ b/mapx/hashmap.go @@ -17,12 +17,12 @@ package mapx import "github.com/ecodeclub/ekit/syncx" type node[T Hashable, ValType any] struct { - key Hashable + key T value ValType next *node[T, ValType] } -func (m *HashMap[T, ValType]) newNode(key Hashable, val ValType) *node[T, ValType] { +func (m *HashMap[T, ValType]) newNode(key T, val ValType) *node[T, ValType] { newNode := m.nodePool.Get() newNode.value = val newNode.key = key @@ -83,8 +83,8 @@ func (m *HashMap[T, ValType]) Get(key T) (ValType, bool) { // Keys 返回 Hashmap 里面的所有的 key。 // 注意:key 的顺序是随机的。 -func (m *HashMap[T, ValType]) Keys() []Hashable { - res := make([]Hashable, 0) +func (m *HashMap[T, ValType]) Keys() []T { + res := make([]T, 0) for _, bucketNode := range m.hashmap { curNode := bucketNode for curNode != nil { diff --git a/mapx/hashmap_test.go b/mapx/hashmap_test.go index edd23953..51161644 100644 --- a/mapx/hashmap_test.go +++ b/mapx/hashmap_test.go @@ -22,6 +22,9 @@ import ( "github.com/stretchr/testify/assert" ) +// 借助 testData 来验证一下 HashMap 实现了 mapi 接口 +var _ mapi[testData, int] = &HashMap[testData, int]{} + func TestHashMap(t *testing.T) { testKV := []struct { key testData @@ -541,5 +544,4 @@ func BenchmarkMyHashMap(b *testing.B) { _ = m[uint64(i)] } }) - } From 25c9c724008d3fbbf6b822330d825c8f8994b3d6 Mon Sep 17 00:00:00 2001 From: Mingyong Chen <67659676+chenmingyong0423@users.noreply.github.com> Date: Wed, 24 May 2023 23:13:28 +0800 Subject: [PATCH 11/32] mapx: MutipleTreeMap (#187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * reflectx: IsNil 方法 - 实现了 IsNilValue 方法,执行 IsNil 方法之前,先对类型进行判断,避免 panic。 - 编写 IsNilValue 方法的测试用例 * reflectx: IsNil 方法 - 修复 nil func 测试用例 * 执行 make check 和 make lint 命令,完成代码格式化和 golintci 的静态代码检 * 添加 CHANGELOG * reflectx: IsNil 方法 - switch 语句里新增 case → reflect.UnsafePointer - 新增 nil UnsafePointer 和 非 nil UnsafePointer 测试用例 * retry: Strategy 接口设计与等间隔重试实现 - 创建 Strategy 重试策略接口 - 实现 EqualRetryStrategy 等时间间隔重试策略 - 编写 func (s *EqualRetryStrategy) Next() * retry: Strategy 接口设计与等间隔重试实现 - EqualRetryStrategy 变更为 FixedIntervalRetryStrategy - 新增 NewErrInvalidIntervalValue 内部方法,返回无效的间隔时间的 error - NewFixedIntervalRetryStrategy 新增对 interval 校验的逻辑 - 原子操作替代锁 * retry: Strategy 接口设计与等间隔重试实现 - TestEqualRetryStrategy_Next 变更为 TestFixedIntervalRetryStrategy_Next * retry: Strategy 接口设计与等间隔重试实现 - TestNewFixedIntervalRetryStrategy 变更为 TestFixedIntervalRetryStrategy_New * retry: 指数退避重试策略实现 - 新增指数退避重试策略(ExponentialBackoffRetryStrategy) - 编写指数退避策略相关函数和方法的测试用例 * retry: 指数退避重试策略实现 - ExponentialBackoffRetryStrategy 结构体新增原子类型字段 maxIntervalReached,用于标记是否已经达到最大重试间隔,后续通过此字段判断如果达到最大重试间隔,则不需要再计算 interval - 测试用例优化 * retry: 指数退避重试策略实现 - 测试用例优化 * retry: 指数退避重试策略实现 - 测试用例函数优化 * mapx: MutipleTreeMap - 实现 MultiMap,一个多映射的 Map - 提供 NewMultiTreeMap 和 NewMultiHashMap 方法 - 为新增函数和方法编写测试用例 * mapx: MutipleTreeMap - 实现 MultiMap,一个多映射的 Map - 提供 NewMultiTreeMap 和 NewMultiHashMap 方法 - 为新增函数和方法编写测试用例 * mapx: MutipleTreeMap - 实现 MultiMap,一个多映射的 Map - 提供 NewMultiTreeMap 和 NewMultiHashMap 方法 - 为新增函数和方法编写测试用例 --------- Co-authored-by: 陈明勇 Co-authored-by: root --- .CHANGELOG.md | 1 + mapx/multiMap.go | 83 ++++++++ mapx/multiMap_test.go | 463 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 547 insertions(+) create mode 100644 mapx/multiMap.go create mode 100644 mapx/multiMap_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 867ab65d..954ab698 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -6,6 +6,7 @@ - [mapx: 修复红黑树删除节点问题](https://github.com/ecodeclub/ekit/pull/183) - [sqlx: 构建Scanner抽象替代现有ScanRows及ScanAll](https://github.com/ecodeclub/ekit/pull/182) - [pool: 重构TaskPool](https://github.com/ecodeclub/ekit/pull/184) +- [mapx: MutipleTreeMap](https://github.com/ecodeclub/ekit/pull/187) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/mapx/multiMap.go b/mapx/multiMap.go new file mode 100644 index 00000000..a36b3f0f --- /dev/null +++ b/mapx/multiMap.go @@ -0,0 +1,83 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapx + +import ( + "github.com/ecodeclub/ekit" +) + +// MultiMap 多映射的 Map +// 它可以将一个键映射到多个值上 +type MultiMap[K any, V any] struct { + m mapi[K, []V] +} + +// NewMultiTreeMap 创建一个基于 TreeMap 的 MultiMap +// 注意: +// - comparator 不能为 nil +func NewMultiTreeMap[K any, V any](comparator ekit.Comparator[K]) (*MultiMap[K, V], error) { + treeMap, err := NewTreeMap[K, []V](comparator) + if err != nil { + return nil, err + } + return &MultiMap[K, V]{ + m: treeMap, + }, nil +} + +// NewMultiHashMap 创建一个基于 HashMap 的 MultiMap +func NewMultiHashMap[K Hashable, V any](size int) *MultiMap[K, V] { + var m mapi[K, []V] = NewHashMap[K, []V](size) + return &MultiMap[K, V]{ + m: m, + } +} + +// Put 在 MultiMap 中添加键值对或向已有键 k 的值追加数据 +func (m *MultiMap[K, V]) Put(k K, v V) error { + val, _ := m.Get(k) + val = append(val, v) + return m.m.Put(k, val) +} + +// Get 从 MultiMap 中获取已有键 k 的值 +// 如果键 k 不存在,则返回的 bool 值为 false +// 返回的切片是一个副本,你对该切片的修改不会影响原本的数据。 +func (m *MultiMap[K, V]) Get(k K) ([]V, bool) { + if v, ok := m.m.Get(k); ok { + return append([]V{}, v...), ok + } + return nil, false +} + +// Delete 从 MultiMap 中删除指定的键 k +func (m *MultiMap[K, V]) Delete(k K) ([]V, bool) { + return m.m.Delete(k) +} + +// Keys 返回 MultiMap 所有的键 +func (m *MultiMap[K, V]) Keys() []K { + return m.m.Keys() +} + +// Values 返回 MultiMap 所有的值 +func (m *MultiMap[K, V]) Values() [][]V { + values := m.m.Values() + copyValues := make([][]V, 0, len(values)) + for i := range values { + copyValues = append(copyValues, append([]V{}, values[i]...)) + } + return copyValues +} diff --git a/mapx/multiMap_test.go b/mapx/multiMap_test.go new file mode 100644 index 00000000..e4549c7f --- /dev/null +++ b/mapx/multiMap_test.go @@ -0,0 +1,463 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapx + +import ( + "testing" + + "github.com/ecodeclub/ekit" + "github.com/stretchr/testify/assert" +) + +func getMultiTreeMap() *MultiMap[int, int] { + multiTreeMap, _ := NewMultiTreeMap[int, int](ekit.ComparatorRealNumber[int]) + return multiTreeMap +} +func getMultiHashMap() *MultiMap[testData, int] { + return NewMultiHashMap[testData, int](10) +} + +func TestMultiMap_NewMultiHashMap(t *testing.T) { + testCases := []struct { + name string + size int + }{ + { + name: "negative size", + size: -1, + }, + { + name: "zero size", + size: 0, + }, + { + name: "Positive size", + size: 1, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + multiMap := NewMultiHashMap[testData, int](tt.size) + assert.NotNil(t, multiMap) + }) + } +} + +func TestMultiMap_NewMultiTreeMap(t *testing.T) { + testCases := []struct { + name string + comparator ekit.Comparator[int] + + wantErr error + }{ + { + name: "no error", + comparator: ekit.ComparatorRealNumber[int], + + wantErr: nil, + }, + { + name: "match errMultiMapComparatorIsNull error", + comparator: nil, + + wantErr: errTreeMapComparatorIsNull, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + multiMap, err := NewMultiTreeMap[int, int](tt.comparator) + assert.Equal(t, tt.wantErr, err) + if err != nil { + assert.Nil(t, multiMap) + } else { + assert.NotNil(t, multiMap) + } + }) + } +} + +func TestMultiMap_Keys(t *testing.T) { + testCases := []struct { + name string + multiTreeMap *MultiMap[int, int] + multiHashMap *MultiMap[testData, int] + + wantMultiTreeMapKeys []int + wantMultiHashMapKeys []testData + }{ + { + name: "empty", + multiTreeMap: func() *MultiMap[int, int] { + return getMultiTreeMap() + }(), + multiHashMap: func() *MultiMap[testData, int] { + return getMultiHashMap() + }(), + + wantMultiTreeMapKeys: []int{}, + wantMultiHashMapKeys: []testData{}, + }, + { + name: "single one", + multiTreeMap: func() *MultiMap[int, int] { + multiTreeMap := getMultiTreeMap() + _ = multiTreeMap.Put(1, 1) + return multiTreeMap + }(), + multiHashMap: func() *MultiMap[testData, int] { + multiHashMap := getMultiHashMap() + _ = multiHashMap.Put(testData{id: 1}, 1) + return multiHashMap + }(), + + wantMultiTreeMapKeys: []int{1}, + wantMultiHashMapKeys: []testData{{id: 1}}, + }, + { + name: "multiple", + multiTreeMap: func() *MultiMap[int, int] { + multiTreeMap := getMultiTreeMap() + _ = multiTreeMap.Put(1, 1) + _ = multiTreeMap.Put(2, 2) + _ = multiTreeMap.Put(3, 3) + _ = multiTreeMap.Put(4, 4) + return multiTreeMap + }(), + multiHashMap: func() *MultiMap[testData, int] { + multiHashMap := getMultiHashMap() + _ = multiHashMap.Put(testData{id: 1}, 1) + _ = multiHashMap.Put(testData{id: 2}, 2) + _ = multiHashMap.Put(testData{id: 3}, 3) + _ = multiHashMap.Put(testData{id: 4}, 4) + return multiHashMap + }(), + + wantMultiTreeMapKeys: []int{1, 2, 3, 4}, + wantMultiHashMapKeys: []testData{ + {id: 1}, + {id: 2}, + {id: 3}, + {id: 4}, + }, + }, + } + for _, tt := range testCases { + t.Run("MultiTreeMap", func(t *testing.T) { + assert.ElementsMatch(t, tt.wantMultiTreeMapKeys, tt.multiTreeMap.Keys()) + }) + + t.Run("MultiHashMap", func(t *testing.T) { + assert.ElementsMatch(t, tt.wantMultiHashMapKeys, tt.multiHashMap.Keys()) + }) + } +} + +func TestMultiMap_Values(t *testing.T) { + testCases := []struct { + name string + multiTreeMap *MultiMap[int, int] + multiHashMap *MultiMap[testData, int] + + wantValues [][]int + }{ + { + name: "empty", + multiTreeMap: func() *MultiMap[int, int] { + return getMultiTreeMap() + }(), + multiHashMap: func() *MultiMap[testData, int] { + return getMultiHashMap() + }(), + + wantValues: [][]int{}, + }, + { + name: "single one", + multiTreeMap: func() *MultiMap[int, int] { + multiTreeMap := getMultiTreeMap() + _ = multiTreeMap.Put(1, 1) + return multiTreeMap + }(), + multiHashMap: func() *MultiMap[testData, int] { + multiHashMap := getMultiHashMap() + _ = multiHashMap.Put(testData{id: 1}, 1) + return multiHashMap + }(), + + wantValues: [][]int{{1}}, + }, + { + name: "multiple", + multiTreeMap: func() *MultiMap[int, int] { + multiTreeMap := getMultiTreeMap() + _ = multiTreeMap.Put(1, 1) + _ = multiTreeMap.Put(2, 2) + _ = multiTreeMap.Put(3, 3) + return multiTreeMap + }(), + multiHashMap: func() *MultiMap[testData, int] { + multiHashMap := getMultiHashMap() + _ = multiHashMap.Put(testData{id: 1}, 1) + _ = multiHashMap.Put(testData{id: 2}, 2) + _ = multiHashMap.Put(testData{id: 3}, 3) + return multiHashMap + }(), + + wantValues: [][]int{{1}, {2}, {3}}, + }, + } + t.Run("MultiTreeMap", func(t *testing.T) { + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.wantValues, tt.multiTreeMap.Values()) + }) + } + }) + t.Run("MultiHashMap", func(t *testing.T) { + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.wantValues, tt.multiHashMap.Values()) + }) + } + }) + +} + +func TestMultiMap_Put(t *testing.T) { + testCases := []struct { + name string + keys []int + values []int + + wantKeys []int + wantValues [][]int + wantErr error + }{ + { + name: "put simple one", + keys: []int{1}, + values: []int{1}, + + wantKeys: []int{1}, + wantValues: [][]int{{1}}, + wantErr: nil, + }, + { + name: "put multiple", + keys: []int{1, 2, 3, 4}, + values: []int{1, 2, 3, 4}, + + wantKeys: []int{1, 2, 3, 4}, + wantValues: [][]int{{1}, {2}, {3}, {4}}, + wantErr: nil, + }, + { + name: "the key include the same", + keys: []int{1, 2, 1, 4}, + values: []int{1, 2, 3, 4}, + + wantKeys: []int{1, 2, 4}, + wantValues: [][]int{ + {1, 3}, + {2}, + {4}, + }, + wantErr: nil, + }, + } + for _, tt := range testCases { + t.Run("MultiTreeMap", func(t *testing.T) { + multiTreeMap, _ := NewMultiTreeMap[int, int](ekit.ComparatorRealNumber[int]) + for i := range tt.keys { + err := multiTreeMap.Put(tt.keys[i], tt.values[i]) + assert.Equal(t, tt.wantErr, err) + } + + for i := range tt.wantKeys { + v, b := multiTreeMap.Get(tt.wantKeys[i]) + assert.Equal(t, true, b) + assert.Equal(t, tt.wantValues[i], v) + } + }) + + t.Run("MultiHashMap", func(t *testing.T) { + multiHashMap := NewMultiHashMap[testData, int](10) + for i := range tt.keys { + err := multiHashMap.Put(testData{id: tt.keys[i]}, tt.values[i]) + assert.Equal(t, tt.wantErr, err) + } + + for i := range tt.wantKeys { + v, b := multiHashMap.Get(testData{id: tt.wantKeys[i]}) + assert.Equal(t, true, b) + assert.Equal(t, tt.wantValues[i], v) + } + }) + } +} + +func TestMultiMap_Get(t *testing.T) { + testCases := []struct { + name string + multiTreeMap *MultiMap[int, int] + multiHashMap *MultiMap[testData, int] + key int + + wantValue []int + wantBool bool + }{ + { + name: "not found (nil) in empty data", + multiTreeMap: func() *MultiMap[int, int] { + return getMultiTreeMap() + }(), + multiHashMap: func() *MultiMap[testData, int] { + return getMultiHashMap() + }(), + key: 1, + + wantValue: nil, + wantBool: false, + }, + { + name: "not found (nil) in data", + multiTreeMap: func() *MultiMap[int, int] { + multiTreeMap := getMultiTreeMap() + _ = multiTreeMap.Put(1, 1) + _ = multiTreeMap.Put(2, 2) + return multiTreeMap + }(), + multiHashMap: func() *MultiMap[testData, int] { + multiHashMap := getMultiHashMap() + _ = multiHashMap.Put(testData{id: 1}, 1) + _ = multiHashMap.Put(testData{id: 2}, 2) + return multiHashMap + }(), + key: 3, + + wantValue: nil, + wantBool: false, + }, + { + name: "found data", + multiTreeMap: func() *MultiMap[int, int] { + multiTreeMap := getMultiTreeMap() + _ = multiTreeMap.Put(1, 1) + return multiTreeMap + }(), + multiHashMap: func() *MultiMap[testData, int] { + multiHashMap := getMultiHashMap() + _ = multiHashMap.Put(testData{id: 1}, 1) + return multiHashMap + }(), + key: 1, + + wantValue: []int{1}, + wantBool: true, + }, + } + for _, tt := range testCases { + t.Run("MultiTreeMap", func(t *testing.T) { + v, b := tt.multiTreeMap.Get(tt.key) + assert.Equal(t, tt.wantBool, b) + assert.ElementsMatch(t, tt.wantValue, v) + }) + + t.Run("MultiHashMap", func(t *testing.T) { + v2, b2 := tt.multiHashMap.Get(testData{id: tt.key}) + assert.Equal(t, tt.wantBool, b2) + assert.ElementsMatch(t, tt.wantValue, v2) + }) + } +} + +func TestMultiMap_Delete(t *testing.T) { + testCases := []struct { + name string + multiTreeMap *MultiMap[int, int] + multiHashMap *MultiMap[testData, int] + + key int + + delValue []int + wantBool bool + }{ + { + name: "not found in empty data", + multiTreeMap: func() *MultiMap[int, int] { + return getMultiTreeMap() + }(), + multiHashMap: func() *MultiMap[testData, int] { + return getMultiHashMap() + }(), + + key: 1, + + delValue: nil, + wantBool: false, + }, + { + name: "not found in data", + multiTreeMap: func() *MultiMap[int, int] { + multiTreeMap := getMultiTreeMap() + _ = multiTreeMap.Put(1, 1) + return multiTreeMap + }(), + multiHashMap: func() *MultiMap[testData, int] { + multiHashMap := getMultiHashMap() + _ = multiHashMap.Put(testData{id: 1}, 1) + return multiHashMap + }(), + + key: 2, + + delValue: nil, + wantBool: false, + }, + { + name: "found and deleted", + multiTreeMap: func() *MultiMap[int, int] { + multiTreeMap := getMultiTreeMap() + _ = multiTreeMap.Put(1, 1) + _ = multiTreeMap.Put(2, 2) + return multiTreeMap + }(), + multiHashMap: func() *MultiMap[testData, int] { + multiHashMap := getMultiHashMap() + _ = multiHashMap.Put(testData{id: 1}, 1) + _ = multiHashMap.Put(testData{id: 2}, 2) + return multiHashMap + }(), + key: 1, + + delValue: []int{1}, + wantBool: true, + }, + } + for _, tt := range testCases { + t.Run("MultiTreeMap", func(t *testing.T) { + v, b := tt.multiTreeMap.Delete(tt.key) + assert.Equal(t, tt.wantBool, b) + assert.ElementsMatch(t, tt.delValue, v) + }) + t.Run("MultiHashMap", func(t *testing.T) { + v, b := tt.multiHashMap.Delete(testData{id: tt.key}) + assert.Equal(t, tt.wantBool, b) + assert.ElementsMatch(t, tt.delValue, v) + }) + } +} From e671c5fdd2d140d3cd7831742db9f64208ba68d9 Mon Sep 17 00:00:00 2001 From: Mingyong Chen <67659676+chenmingyong0423@users.noreply.github.com> Date: Tue, 30 May 2023 13:32:25 +0800 Subject: [PATCH 12/32] =?UTF-8?q?mapx:=20=E4=B8=BA=20MultipleMap=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20PutVals=20=E6=96=B9=E6=B3=95=20(#189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: 陈明勇 Co-authored-by: root --- .CHANGELOG.md | 1 + mapx/multiMap.go | 7 ++- mapx/multiMap_test.go | 106 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 954ab698..83f8064f 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -7,6 +7,7 @@ - [sqlx: 构建Scanner抽象替代现有ScanRows及ScanAll](https://github.com/ecodeclub/ekit/pull/182) - [pool: 重构TaskPool](https://github.com/ecodeclub/ekit/pull/184) - [mapx: MutipleTreeMap](https://github.com/ecodeclub/ekit/pull/187) +- [mapx: 为 MultipleMap 添加 PutVals 方法](https://github.com/ecodeclub/ekit/pull/189) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/mapx/multiMap.go b/mapx/multiMap.go index a36b3f0f..2a3fcbec 100644 --- a/mapx/multiMap.go +++ b/mapx/multiMap.go @@ -47,8 +47,13 @@ func NewMultiHashMap[K Hashable, V any](size int) *MultiMap[K, V] { // Put 在 MultiMap 中添加键值对或向已有键 k 的值追加数据 func (m *MultiMap[K, V]) Put(k K, v V) error { + return m.PutMany(k, v) +} + +// PutMany 在 MultiMap 中添加键值对或向已有键 k 的值追加多个数据 +func (m *MultiMap[K, V]) PutMany(k K, v ...V) error { val, _ := m.Get(k) - val = append(val, v) + val = append(val, v...) return m.m.Put(k, val) } diff --git a/mapx/multiMap_test.go b/mapx/multiMap_test.go index e4549c7f..754e6327 100644 --- a/mapx/multiMap_test.go +++ b/mapx/multiMap_test.go @@ -461,3 +461,109 @@ func TestMultiMap_Delete(t *testing.T) { }) } } + +func TestMultiMap_PutMany(t *testing.T) { + testCases := []struct { + name string + keys []int + values [][]int + + wantKeys []int + wantValues [][]int + wantErr error + }{ + { + name: "one to one", + keys: []int{1}, + values: [][]int{{1}}, + + wantKeys: []int{1}, + wantValues: [][]int{{1}}, + wantErr: nil, + }, + { + name: "many [one to one]", + keys: []int{1, 2, 3}, + values: [][]int{{1}, {2}, {3}}, + + wantKeys: []int{1, 2, 3}, + wantValues: [][]int{{1}, {2}, {3}}, + wantErr: nil, + }, + { + name: "one to many", + keys: []int{1}, + values: [][]int{{1, 2, 3}}, + + wantKeys: []int{1}, + wantValues: [][]int{ + {1, 2, 3}, + }, + wantErr: nil, + }, + { + name: "many [one to many]", + keys: []int{1, 2, 3}, + values: [][]int{{1, 2, 3}, {1, 2, 3}, {1, 2, 3}}, + + wantKeys: []int{1, 2, 3}, + wantValues: [][]int{ + {1, 2, 3}, + {1, 2, 3}, + {1, 2, 3}, + }, + wantErr: nil, + }, + { + name: "the key include the same for append one", + keys: []int{1, 1}, + values: [][]int{{1, 2, 3, 4, 5}, {6}}, + + wantKeys: []int{1}, + wantValues: [][]int{ + {1, 2, 3, 4, 5, 6}, + }, + wantErr: nil, + }, + { + name: "the key include the same for append many", + keys: []int{1, 1}, + values: [][]int{{1}, {2, 3, 4, 5, 6}}, + + wantKeys: []int{1}, + wantValues: [][]int{ + {1, 2, 3, 4, 5, 6}, + }, + wantErr: nil, + }, + } + for _, tt := range testCases { + t.Run("MultiTreeMap", func(t *testing.T) { + multiTreeMap, _ := NewMultiTreeMap[int, int](ekit.ComparatorRealNumber[int]) + for i := range tt.keys { + err := multiTreeMap.PutMany(tt.keys[i], tt.values[i]...) + assert.Equal(t, tt.wantErr, err) + } + + for i := range tt.wantKeys { + v, b := multiTreeMap.Get(tt.wantKeys[i]) + assert.Equal(t, true, b) + assert.Equal(t, tt.wantValues[i], v) + } + }) + + t.Run("MultiHashMap", func(t *testing.T) { + multiHashMap := NewMultiHashMap[testData, int](10) + for i := range tt.keys { + err := multiHashMap.PutMany(testData{id: tt.keys[i]}, tt.values[i]...) + assert.Equal(t, tt.wantErr, err) + } + + for i := range tt.wantKeys { + v, b := multiHashMap.Get(testData{id: tt.wantKeys[i]}) + assert.Equal(t, true, b) + assert.Equal(t, tt.wantValues[i], v) + } + }) + } +} From cd5a84c7383cd6e1c02c1521c8e56d75ffe782ef Mon Sep 17 00:00:00 2001 From: Mingyong Chen <67659676+chenmingyong0423@users.noreply.github.com> Date: Mon, 12 Jun 2023 22:48:18 +0800 Subject: [PATCH 13/32] =?UTF-8?q?mapx:=20LinkedMap=20=E7=89=B9=E6=80=A7=20?= =?UTF-8?q?(#191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mapx: LinkedMap 特性 - 新增 LinkedMap 结构体 - 为 LinkedMap 实现 mapi 接口的方法 - 为新增方法添加测试方法 closes #190 --------- Co-authored-by: root --- .CHANGELOG.md | 1 + mapx/linkedmap.go | 112 ++++++++++ mapx/linkedmap_test.go | 469 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 582 insertions(+) create mode 100644 mapx/linkedmap.go create mode 100644 mapx/linkedmap_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 83f8064f..aed71a15 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -8,6 +8,7 @@ - [pool: 重构TaskPool](https://github.com/ecodeclub/ekit/pull/184) - [mapx: MutipleTreeMap](https://github.com/ecodeclub/ekit/pull/187) - [mapx: 为 MultipleMap 添加 PutVals 方法](https://github.com/ecodeclub/ekit/pull/189) +- [mapx: LinkedMap 特性](https://github.com/ecodeclub/ekit/pull/191) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/mapx/linkedmap.go b/mapx/linkedmap.go new file mode 100644 index 00000000..127f32af --- /dev/null +++ b/mapx/linkedmap.go @@ -0,0 +1,112 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapx + +import "github.com/ecodeclub/ekit" + +type LinkedMap[K any, V any] struct { + m mapi[K, *linkedKV[K, V]] + head, tail *linkedKV[K, V] + length int +} + +type linkedKV[K any, V any] struct { + key K + value V + prev, next *linkedKV[K, V] +} + +func NewLinkedHashMap[K Hashable, V any](size int) *LinkedMap[K, V] { + hashmap := NewHashMap[K, *linkedKV[K, V]](size) + head := &linkedKV[K, V]{} + tail := &linkedKV[K, V]{next: head, prev: head} + head.prev, head.next = tail, tail + return &LinkedMap[K, V]{ + m: hashmap, + head: head, + tail: tail, + } +} + +func NewLinkedTreeMap[K any, V any](comparator ekit.Comparator[K]) (*LinkedMap[K, V], error) { + treeMap, err := NewTreeMap[K, *linkedKV[K, V]](comparator) + if err != nil { + return nil, err + } + head := &linkedKV[K, V]{} + tail := &linkedKV[K, V]{next: head, prev: head} + head.prev, head.next = tail, tail + return &LinkedMap[K, V]{ + m: treeMap, + head: head, + tail: tail, + }, nil +} + +func (l *LinkedMap[K, V]) Put(key K, val V) error { + if lk, ok := l.m.Get(key); ok { + lk.value = val + return nil + } + lk := &linkedKV[K, V]{ + key: key, + value: val, + prev: l.tail.prev, + next: l.tail, + } + if err := l.m.Put(key, lk); err != nil { + return err + } + lk.prev.next, lk.next.prev = lk, lk + l.length++ + return nil +} + +func (l *LinkedMap[K, V]) Get(key K) (V, bool) { + if lk, ok := l.m.Get(key); ok { + return lk.value, ok + } + var v V + return v, false +} + +func (l *LinkedMap[K, V]) Delete(key K) (V, bool) { + if lk, ok := l.m.Delete(key); ok { + lk.prev.next = lk.next + lk.next.prev = lk.prev + l.length-- + return lk.value, ok + } + var v V + return v, false +} + +func (l *LinkedMap[K, V]) Keys() []K { + keys := make([]K, 0, l.length) + for cur := l.head.next; cur != l.tail; { + keys = append(keys, cur.key) + cur = cur.next + } + return keys +} + +func (l *LinkedMap[K, V]) Values() []V { + values := make([]V, 0, l.length) + for cur := l.head.next; cur != l.tail; { + values = append(values, cur.value) + cur = cur.next + } + return values +} diff --git a/mapx/linkedmap_test.go b/mapx/linkedmap_test.go new file mode 100644 index 00000000..a0060f3a --- /dev/null +++ b/mapx/linkedmap_test.go @@ -0,0 +1,469 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapx + +import ( + "errors" + "testing" + + "github.com/ecodeclub/ekit" + "github.com/stretchr/testify/assert" +) + +var ( + fakeErr = errors.New("fakeMap: put error") +) + +type fakeMap[K any, V any] struct { + *LinkedMap[K, V] + count int + activeFirstErr bool +} + +func (f *fakeMap[K, V]) Put(key K, val V) error { + f.count++ + if f.activeFirstErr { + f.activeFirstErr = false + return fakeErr + } + if f.count == 3 { + return fakeErr + } + if f.count == 5 { + return fakeErr + } + return f.LinkedMap.Put(key, val) +} + +func newLinkedFakeMap[K any, V any](activeFirstErr bool, comparator ekit.Comparator[K]) (*LinkedMap[K, V], error) { + treeMap, err := NewLinkedTreeMap[K, *linkedKV[K, V]](comparator) + if err != nil { + return nil, err + } + fm := &fakeMap[K, *linkedKV[K, V]]{LinkedMap: treeMap, activeFirstErr: activeFirstErr} + head := &linkedKV[K, V]{} + tail := &linkedKV[K, V]{next: head, prev: head} + head.prev, head.next = tail, tail + return &LinkedMap[K, V]{ + m: fm, + head: head, + tail: tail, + }, nil +} + +func TestLinkedMap_NewLinkedHashMap(t *testing.T) { + testCases := []struct { + name string + size int + }{ + { + name: "negative size", + size: -1, + }, + { + name: "zero size", + size: 0, + }, + { + name: "Positive size", + size: 1, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + multiMap := NewLinkedHashMap[testData, int](tt.size) + assert.NotNil(t, multiMap) + assert.Equal(t, multiMap.Keys(), []testData{}) + assert.Equal(t, multiMap.Values(), []int{}) + }) + } +} + +func TestLinkedMap_NewLinkedTreeMap(t *testing.T) { + testCases := []struct { + name string + comparator ekit.Comparator[int] + + wantErr error + }{ + { + name: "no error", + comparator: ekit.ComparatorRealNumber[int], + + wantErr: nil, + }, + { + name: "match errLinkedTreeMapComparatorIsNull error", + comparator: nil, + + wantErr: errTreeMapComparatorIsNull, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + linkedTreeMap, err := NewLinkedTreeMap[int, int](tt.comparator) + assert.Equal(t, tt.wantErr, err) + if err != nil { + assert.Nil(t, linkedTreeMap) + } else { + assert.NotNil(t, linkedTreeMap) + assert.Equal(t, linkedTreeMap.Keys(), []int{}) + assert.Equal(t, linkedTreeMap.Values(), []int{}) + } + }) + } +} + +func TestLinkedMap_Put(t *testing.T) { + testCases := []struct { + name string + linkedMap func(t *testing.T) *LinkedMap[int, int] + keys []int + values []int + + wantKeys []int + wantValues []int + wantErrs []error + }{ + { + name: "put single key", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + return linkedTreeMap + }, + keys: []int{1}, + values: []int{1}, + + wantKeys: []int{1}, + wantValues: []int{1}, + wantErrs: []error{nil}, + }, + { + name: "put multiple keys", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + return linkedTreeMap + }, + keys: []int{1, 2, 3, 4}, + values: []int{1, 2, 3, 4}, + + wantKeys: []int{1, 2, 3, 4}, + wantValues: []int{1, 2, 3, 4}, + wantErrs: []error{nil, nil, nil, nil}, + }, + { + name: "change value of single key", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + return linkedTreeMap + }, + keys: []int{1, 1, 2, 3}, + values: []int{1, 11, 2, 3}, + + wantKeys: []int{1, 2, 3}, + wantValues: []int{11, 2, 3}, + wantErrs: []error{nil, nil, nil, nil}, + }, + { + name: "change value of multiple keys", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + return linkedTreeMap + }, + keys: []int{1, 1, 2, 2, 3}, + values: []int{1, 11, 2, 22, 3}, + + wantKeys: []int{1, 2, 3}, + wantValues: []int{11, 22, 3}, + wantErrs: []error{nil, nil, nil, nil, nil}, + }, + { + name: "get error when put single key", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedFakeMap, err := newLinkedFakeMap[int, int](true, ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + return linkedFakeMap + }, + keys: []int{1}, + values: []int{1}, + + wantKeys: []int{}, + wantValues: []int{}, + wantErrs: []error{fakeErr}, + }, + { + name: "get multiple errors when put multiple keys", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedFakeMap, err := newLinkedFakeMap[int, int](true, ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + return linkedFakeMap + }, + keys: []int{1, 2, 3, 4, 5}, + values: []int{1, 2, 3, 4, 5}, + + wantKeys: []int{2, 4}, + wantValues: []int{2, 4}, + wantErrs: []error{fakeErr, nil, fakeErr, nil, fakeErr}, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + errs := make([]error, 0) + linkedMap := tt.linkedMap(t) + for i := range tt.keys { + err := linkedMap.Put(tt.keys[i], tt.values[i]) + errs = append(errs, err) + } + + for i := range tt.wantKeys { + v, b := linkedMap.Get(tt.wantKeys[i]) + assert.Equal(t, true, b) + assert.Equal(t, tt.wantValues[i], v) + } + + assert.Equal(t, tt.wantKeys, linkedMap.Keys()) + assert.Equal(t, tt.wantValues, linkedMap.Values()) + assert.Equal(t, tt.wantErrs, errs) + }) + } +} + +func TestLinkedMap_Get(t *testing.T) { + testCases := []struct { + name string + linkedMap func(t *testing.T) *LinkedMap[int, int] + key int + + wantValue int + wantBool bool + }{ + { + name: "can not find value in empty linked map", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + return linkedTreeMap + }, + key: 1, + + wantValue: 0, + wantBool: false, + }, + { + name: "can not find value in linked map", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + err = linkedTreeMap.Put(1, 1) + assert.NoError(t, err) + return linkedTreeMap + }, + key: 2, + + wantValue: 0, + wantBool: false, + }, + { + name: "find value in linked map", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + err = linkedTreeMap.Put(1, 1) + assert.NoError(t, err) + return linkedTreeMap + }, + key: 1, + + wantValue: 1, + wantBool: true, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + v, b := tt.linkedMap(t).Get(tt.key) + assert.Equal(t, tt.wantBool, b) + assert.Equal(t, tt.wantValue, v) + }) + } +} + +func TestLinkedMap_Delete(t *testing.T) { + testCases := []struct { + name string + linkedMap func(t *testing.T) *LinkedMap[int, int] + + key int + + delValue int + wantBool bool + wantKeys []int + wantValues []int + }{ + { + name: "delete key in empty linked map", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + return linkedTreeMap + }, + + key: 1, + + delValue: 0, + wantBool: false, + wantKeys: []int{}, + wantValues: []int{}, + }, + { + name: "delete unknown key in not empty linked map", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + assert.NoError(t, linkedTreeMap.Put(1, 1)) + return linkedTreeMap + }, + + key: 2, + + delValue: 0, + wantBool: false, + wantKeys: []int{1}, + wantValues: []int{1}, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + linkedMap := tt.linkedMap(t) + v, b := linkedMap.Delete(tt.key) + assert.Equal(t, tt.wantBool, b) + assert.Equal(t, tt.delValue, v) + + assert.Equal(t, tt.wantKeys, linkedMap.Keys()) + assert.Equal(t, tt.wantValues, linkedMap.Values()) + }) + } +} + +func TestLinkedMap_PutAndDelete(t *testing.T) { + testCases := []struct { + name string + linkedMap func(t *testing.T) *LinkedMap[int, int] + + wantKeys []int + wantValues []int + }{ + { + name: "put k1 → delete k1", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + assert.NoError(t, linkedTreeMap.Put(1, 1)) + v, ok := linkedTreeMap.Delete(1) + assert.Equal(t, 1, v) + assert.Equal(t, true, ok) + return linkedTreeMap + }, + + wantKeys: []int{}, + wantValues: []int{}, + }, + { + name: "put k1 → put k2 → delete k1", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + assert.NoError(t, linkedTreeMap.Put(1, 1)) + assert.NoError(t, linkedTreeMap.Put(2, 2)) + v, ok := linkedTreeMap.Delete(1) + assert.Equal(t, 1, v) + assert.Equal(t, true, ok) + return linkedTreeMap + }, + + wantKeys: []int{2}, + wantValues: []int{2}, + }, + { + name: "put k1 → put k2 → delete k2", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + assert.NoError(t, linkedTreeMap.Put(1, 1)) + assert.NoError(t, linkedTreeMap.Put(2, 2)) + v, ok := linkedTreeMap.Delete(2) + assert.Equal(t, 2, v) + assert.Equal(t, true, ok) + return linkedTreeMap + }, + + wantKeys: []int{1}, + wantValues: []int{1}, + }, + { + name: "put k1 → delete k1 → put k2 → put k3", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + assert.NoError(t, linkedTreeMap.Put(1, 1)) + v, ok := linkedTreeMap.Delete(1) + assert.Equal(t, 1, v) + assert.Equal(t, true, ok) + assert.NoError(t, linkedTreeMap.Put(2, 2)) + assert.NoError(t, linkedTreeMap.Put(3, 3)) + + return linkedTreeMap + }, + + wantKeys: []int{2, 3}, + wantValues: []int{2, 3}, + }, + { + name: "put k1 → put k2 → put k3 → delete k2", + linkedMap: func(t *testing.T) *LinkedMap[int, int] { + linkedTreeMap, err := NewLinkedTreeMap[int, int](ekit.ComparatorRealNumber[int]) + assert.NoError(t, err) + assert.NoError(t, linkedTreeMap.Put(1, 1)) + assert.NoError(t, linkedTreeMap.Put(2, 2)) + assert.NoError(t, linkedTreeMap.Put(3, 3)) + v, ok := linkedTreeMap.Delete(2) + assert.Equal(t, 2, v) + assert.Equal(t, true, ok) + + return linkedTreeMap + }, + + wantKeys: []int{1, 3}, + wantValues: []int{1, 3}, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + linkedMap := tt.linkedMap(t) + for i := range tt.wantKeys { + v, b := linkedMap.Get(tt.wantKeys[i]) + assert.Equal(t, true, b) + assert.Equal(t, tt.wantValues[i], v) + } + assert.Equal(t, tt.wantKeys, linkedMap.Keys()) + assert.Equal(t, tt.wantValues, linkedMap.Values()) + }) + } +} From 0f2c145681ae3a61b4d97d6790736d893002614f Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Sat, 1 Jul 2023 12:56:34 +0800 Subject: [PATCH 14/32] =?UTF-8?q?syncx:=20Map=20=E6=94=AF=E6=8C=81=20LoadO?= =?UTF-8?q?rStoreFunc=20=E6=96=B9=E6=B3=95=20(#194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .CHANGELOG.md | 1 + syncx/map.go | 22 ++++++++++++++ syncx/map_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index aed71a15..d887b0d5 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -6,6 +6,7 @@ - [mapx: 修复红黑树删除节点问题](https://github.com/ecodeclub/ekit/pull/183) - [sqlx: 构建Scanner抽象替代现有ScanRows及ScanAll](https://github.com/ecodeclub/ekit/pull/182) - [pool: 重构TaskPool](https://github.com/ecodeclub/ekit/pull/184) +- [syncx:Map 支持 LoadOrStoreFunc 方法](https://github.com/ecodeclub/ekit/pull/194) - [mapx: MutipleTreeMap](https://github.com/ecodeclub/ekit/pull/187) - [mapx: 为 MultipleMap 添加 PutVals 方法](https://github.com/ecodeclub/ekit/pull/189) - [mapx: LinkedMap 特性](https://github.com/ecodeclub/ekit/pull/191) diff --git a/syncx/map.go b/syncx/map.go index 9ad07def..428a5964 100644 --- a/syncx/map.go +++ b/syncx/map.go @@ -41,6 +41,7 @@ func (m *Map[K, V]) Store(key K, value V) { } // LoadOrStore 加载或者存储一个键值对 +// true 代表是加载的,false 代表执行了 store func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { var anyVal any anyVal, loaded = m.m.LoadOrStore(key, value) @@ -50,6 +51,27 @@ func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { return } +// LoadOrStoreFunc 是一个优化,也就是使用该方法能够避免无意义的创建实例。 +// 如果你的初始化过程非常消耗资源,那么使用这个方法是有价值的。 +// 它的代价就是 Key 不存在的时候会多一次 Load 调用。 +// 当 fn 返回 error 的时候,LoadOrStoreFunc 也会返回 error。 +func (m *Map[K, V]) LoadOrStoreFunc(key K, fn func() (V, error)) (actual V, loaded bool, err error) { + var anyVal any + val, ok := m.Load(key) + if ok { + return val, true, nil + } + val, err = fn() + if err != nil { + return + } + anyVal, loaded = m.m.LoadOrStore(key, val) + if anyVal != nil { + actual = anyVal.(V) + } + return +} + // LoadAndDelete 加载并且删除一个键值对 func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) { var anyVal any diff --git a/syncx/map_test.go b/syncx/map_test.go index f8d8a20c..73063a0b 100644 --- a/syncx/map_test.go +++ b/syncx/map_test.go @@ -15,6 +15,7 @@ package syncx import ( + "errors" "fmt" "sync" "testing" @@ -87,6 +88,32 @@ func TestMap_LoadOrStore(t *testing.T) { assert.Nil(t, val) } +func TestMap_LoadOrStoreFunc(t *testing.T) { + var m = Map[string, *User]{} + val, loaded, err := m.LoadOrStoreFunc("Tom", func() (*User, error) { + return &User{Name: "Tom"}, nil + }) + assert.NoError(t, err) + assert.False(t, loaded) + assert.Equal(t, &User{Name: "Tom"}, val) + + // 测试 Tom 存在的情况 + val, loaded, err = m.LoadOrStoreFunc("Tom", func() (*User, error) { + return &User{Name: "Tom"}, nil + }) + assert.NoError(t, err) + assert.True(t, loaded) + assert.Equal(t, &User{Name: "Tom"}, val) + + // 测试初始化失败 + val, loaded, err = m.LoadOrStoreFunc("Jerry", func() (*User, error) { + return nil, errors.New("初始话失败") + }) + assert.Equal(t, err, errors.New("初始话失败")) + assert.False(t, loaded) + assert.Equal(t, (*User)(nil), val) +} + func TestMap_LoadAndDelete(t *testing.T) { var m = Map[string, *User]{} m.Store("Tom", nil) @@ -204,6 +231,54 @@ func ExampleMap_LoadOrStore() { // 加载旧值 } +func ExampleMap_LoadOrStoreFunc() { + var m = Map[string, *User]{} + _, loaded, _ := m.LoadOrStoreFunc("Tom", func() (*User, error) { + return &User{Name: "Tom"}, nil + }) + // 执行存储 + if !loaded { + fmt.Println("设置了新值 Tom") + } + + _, loaded, _ = m.LoadOrStoreFunc("Tom", func() (*User, error) { + return &User{Name: "Tom-copy"}, nil + }) + // Tom 这个 key 已经存在,执行加载 + if loaded { + fmt.Println("加载旧值 Tom") + } + + _, loaded, _ = m.LoadOrStoreFunc("Jerry", func() (*User, error) { + return nil, nil + }) + // 执行存储,注意值是 nil + if !loaded { + fmt.Println("设置了新值 nil") + } + val, loaded, _ := m.LoadOrStoreFunc("Jerry", func() (*User, error) { + return &User{Name: "Jerry"}, nil + }) + // Jerry 这个 key 已经存在,执行加载,于是把原本的 nil 加载出来 + if loaded { + fmt.Printf("加载旧值 %v\n", val) + } + + _, _, err := m.LoadOrStoreFunc("Kitty", func() (*User, error) { + return nil, errors.New("初始化失败") + }) + if err != nil { + fmt.Println(err.Error()) + } + + // Output: + // 设置了新值 Tom + // 加载旧值 Tom + // 设置了新值 nil + // 加载旧值 + // 初始化失败 +} + func ExampleMap_Range() { var m Map[string, int] m.Store("Tom", 18) From 72ded6af5d904ac96ac5b7b71e6f2a56a0947084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E6=9F=8A=E6=85=A7=E9=9F=B3?= Date: Wed, 5 Jul 2023 16:53:27 +0800 Subject: [PATCH 15/32] =?UTF-8?q?copier:=20ReflectCopier=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=BF=BD=E7=95=A5=E5=AD=97=E6=AE=B5=20(#196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bean/cpoier add ignore option * bean/copier,1、使用 bean/option 和 set 改造一下忽略字段的需求;2、重新给测试用例命名; * bean/copier,将 options 改成 option 模式的,方便后续别的附加功能可以自定义初始化过程。 * bean/copier,将 options 改成延迟初始化。 * bean/copier,执行 make check;修改 mapset 初始化时的容量;补全测试用例; --- .CHANGELOG.md | 1 + bean/copier/copy.go | 44 ++- bean/copier/reflect_copier.go | 21 +- bean/copier/reflect_copier_test.go | 535 +++++++++++++++++++++++++++++ 4 files changed, 596 insertions(+), 5 deletions(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index d887b0d5..b08b6a30 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -10,6 +10,7 @@ - [mapx: MutipleTreeMap](https://github.com/ecodeclub/ekit/pull/187) - [mapx: 为 MultipleMap 添加 PutVals 方法](https://github.com/ecodeclub/ekit/pull/189) - [mapx: LinkedMap 特性](https://github.com/ecodeclub/ekit/pull/191) +- [copier: ReflectCopier 支持忽略字段](https://github.com/ecodeclub/ekit/pull/196) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/bean/copier/copy.go b/bean/copier/copy.go index 00d4c046..df4c3f32 100644 --- a/bean/copier/copy.go +++ b/bean/copier/copy.go @@ -14,6 +14,11 @@ package copier +import ( + "github.com/ecodeclub/ekit/bean/option" + "github.com/ecodeclub/ekit/set" +) + // Copier 复制数据 // 1. 深拷贝亦或是浅拷贝,取决于具体的实现。每个实现都要声明清楚这一点; // 2. Src 和 Dst 都必须是普通的结构体,支持组合 @@ -21,7 +26,42 @@ package copier // 这种设计设计,即使用 *Src 和 *Dst 可能加剧内存逃逸 type Copier[Src any, Dst any] interface { // CopyTo 将 src 中的数据复制到 dst 中 - CopyTo(src *Src, dst *Dst) error + CopyTo(src *Src, dst *Dst, opts ...option.Option[options]) error // Copy 将创建一个 Dst 的实例,并且将 Src 中的数据复制过去 - Copy(src *Src) (*Dst, error) + Copy(src *Src, opts ...option.Option[options]) (*Dst, error) +} + +// options 执行复制操作时的可选配置 +type options struct { + // ignoreFields 执行复制操作时,需要忽略的字段 + ignoreFields *set.MapSet[string] +} + +func newOptions() *options { + return &options{} +} + +// InIgnoreFields 判断 str 是不是在 ignoreFields 里面 +func (r *options) InIgnoreFields(str string) bool { + // 如果没有设置过忽略的字段的话,ignoreFields 就有可能是 nil,这里需要判断一下 + if r.ignoreFields == nil { + return false + } + return r.ignoreFields.Exist(str) +} + +// IgnoreFields 设置复制时要忽略的字段(option 设计模式) +func IgnoreFields(fields ...string) option.Option[options] { + return func(opt *options) { + if len(fields) < 1 { + return + } + // 需要用的时候再延迟初始化 ignoreFields + if opt.ignoreFields == nil { + opt.ignoreFields = set.NewMapSet[string](len(fields)) + } + for i := 0; i < len(fields); i++ { + opt.ignoreFields.Add(fields[i]) + } + } } diff --git a/bean/copier/reflect_copier.go b/bean/copier/reflect_copier.go index 2c882664..28c7631c 100644 --- a/bean/copier/reflect_copier.go +++ b/bean/copier/reflect_copier.go @@ -16,6 +16,8 @@ package copier import ( "reflect" + + "github.com/ecodeclub/ekit/bean/option" ) // ReflectCopier 基于反射的实现 @@ -24,6 +26,9 @@ type ReflectCopier[Src any, Dst any] struct { // rootField 字典树的根节点 rootField fieldNode + + // options 执行复制操作时的可选配置 + options *options } // fieldNode 字段的前缀树 @@ -142,9 +147,9 @@ func createFieldNodes(root *fieldNode, srcTyp, dstTyp reflect.Type) error { return nil } -func (r *ReflectCopier[Src, Dst]) Copy(src *Src) (*Dst, error) { +func (r *ReflectCopier[Src, Dst]) Copy(src *Src, opts ...option.Option[options]) (*Dst, error) { dst := new(Dst) - err := r.CopyTo(src, dst) + err := r.CopyTo(src, dst, opts...) return dst, err } @@ -154,7 +159,11 @@ func (r *ReflectCopier[Src, Dst]) Copy(src *Src) (*Dst, error) { // 2. 如果 Src 和 Dst 中匹配的字段,其类型是基本类型(及其指针)或者内置类型(及其指针),并且类型一样,则直接用 Src 的值 // 3. 如果 Src 和 Dst 中匹配的字段,其类型都是结构体,或者都是结构体指针,则会深入复制 // 4. 否则,忽略字段 -func (r *ReflectCopier[Src, Dst]) CopyTo(src *Src, dst *Dst) error { +func (r *ReflectCopier[Src, Dst]) CopyTo(src *Src, dst *Dst, opts ...option.Option[options]) error { + opt := newOptions() + option.Apply(opt, opts...) + r.options = opt + return r.copyToWithTree(src, dst) } @@ -191,6 +200,12 @@ func (r *ReflectCopier[Src, Dst]) copyTreeNode(srcTyp reflect.Type, srcValue ref for i := range root.fields { child := &root.fields[i] + + // 只要结构体属性的名字在需要忽略的字段里面,就不走下面的复制逻辑 + if r.options.InIgnoreFields(child.name) { + continue + } + childSrcTyp := srcTyp.Field(child.srcIndex) childSrcValue := srcValue.Field(child.srcIndex) diff --git a/bean/copier/reflect_copier_test.go b/bean/copier/reflect_copier_test.go index d4d9d888..131b9947 100644 --- a/bean/copier/reflect_copier_test.go +++ b/bean/copier/reflect_copier_test.go @@ -505,6 +505,541 @@ func TestReflectCopier_Copy(t *testing.T) { }, wantDst: &SpecialDst2{A: 1}, }, + { + name: "simple_struct_忽略字段的时候传空", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SimpleSrc, SimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy(&SimpleSrc{ + Name: "大明", + Age: ekit.ToPtr[int](18), + Friends: []string{"Tom", "Jerry"}, + }, IgnoreFields()) + }, + wantDst: &SimpleDst{ + Name: "大明", + Age: ekit.ToPtr[int](18), + Friends: []string{"Tom", "Jerry"}, + }, + }, + { + name: "simple_struct_忽略一个字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SimpleSrc, SimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy(&SimpleSrc{ + Name: "大明", + Age: ekit.ToPtr[int](18), + Friends: []string{"Tom", "Jerry"}, + }, IgnoreFields("Age")) + }, + wantDst: &SimpleDst{ + Name: "大明", + Age: nil, + Friends: []string{"Tom", "Jerry"}, + }, + }, + { + name: "simple_struct_忽略多个字段_传入多个Option_每个Option传入一个字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SimpleSrc, SimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy(&SimpleSrc{ + Name: "大明", + Age: ekit.ToPtr[int](18), + Friends: []string{"Tom", "Jerry"}, + }, IgnoreFields("Age"), IgnoreFields("Friends")) + }, + wantDst: &SimpleDst{ + Name: "大明", + Age: nil, + Friends: nil, + }, + }, + { + name: "simple_struct_忽略多个字段_传入一个Option_Option传入多个字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SimpleSrc, SimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy(&SimpleSrc{ + Name: "大明", + Age: ekit.ToPtr[int](18), + Friends: []string{"Tom", "Jerry"}, + }, IgnoreFields("Age", "Friends")) + }, + wantDst: &SimpleDst{ + Name: "大明", + Age: nil, + Friends: nil, + }, + }, + { + name: "simple_struct_忽略全部字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SimpleSrc, SimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy(&SimpleSrc{ + Name: "大明", + Age: ekit.ToPtr[int](18), + Friends: []string{"Tom", "Jerry"}, + }, IgnoreFields("Name"), IgnoreFields("Age"), IgnoreFields("Friends")) + }, + wantDst: &SimpleDst{}, + }, + { + name: "simple_struct_空切片_空指针_忽略字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SimpleSrc, SimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy(&SimpleSrc{ + Name: "大明", + }, IgnoreFields("Name")) + }, + wantDst: &SimpleDst{ + Name: "", + }, + }, + { + name: "组合_struct_忽略组合中的一个字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[EmbedSrc, EmbedDst]() + if err != nil { + return nil, err + } + return copier.Copy(&EmbedSrc{ + SimpleSrc: SimpleSrc{ + Name: "xiaoli", + Age: ekit.ToPtr[int](19), + Friends: []string{}, + }, + BasicSrc: &BasicSrc{ + Name: "xiaowang", + Age: 20, + CNumber: complex(2, 2), + }, + }, IgnoreFields("CNumber")) + }, + wantDst: &EmbedDst{ + SimpleSrc: SimpleSrc{ + Name: "xiaoli", + Age: ekit.ToPtr[int](19), + Friends: []string{}, + }, + BasicSrc: &BasicSrc{ + Name: "xiaowang", + Age: 20, + CNumber: complex(0, 0), + }, + }, + }, + { + name: "组合_struct_忽略组合中全部同名字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[EmbedSrc, EmbedDst]() + if err != nil { + return nil, err + } + return copier.Copy(&EmbedSrc{ + SimpleSrc: SimpleSrc{ + Name: "xiaoli", + Age: ekit.ToPtr[int](19), + Friends: []string{}, + }, + BasicSrc: &BasicSrc{ + Name: "xiaowang", + Age: 20, + CNumber: complex(2, 2), + }, + }, IgnoreFields("Age")) + }, + wantDst: &EmbedDst{ + SimpleSrc: SimpleSrc{ + Name: "xiaoli", + Age: nil, + Friends: []string{}, + }, + BasicSrc: &BasicSrc{ + Name: "xiaowang", + Age: 0, + CNumber: complex(2, 2), + }, + }, + }, + { + name: "组合_struct_忽略组合中同名结构体", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[EmbedSrc, EmbedDst]() + if err != nil { + return nil, err + } + return copier.Copy(&EmbedSrc{ + SimpleSrc: SimpleSrc{ + Name: "xiaoli", + Age: ekit.ToPtr[int](19), + Friends: []string{}, + }, + BasicSrc: &BasicSrc{ + Name: "xiaowang", + Age: 20, + CNumber: complex(2, 2), + }, + }, IgnoreFields("SimpleSrc")) + }, + wantDst: &EmbedDst{ + SimpleSrc: SimpleSrc{}, + BasicSrc: &BasicSrc{ + Name: "xiaowang", + Age: 20, + CNumber: complex(2, 2), + }, + }, + }, + { + name: "复杂_Struct_忽略多层嵌套中全部同名字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ComplexSrc, ComplexDst]() + if err != nil { + return nil, err + } + return copier.Copy(&ComplexSrc{ + Simple: SimpleSrc{ + Name: "xiaohong", + Age: ekit.ToPtr[int](18), + Friends: []string{"ha", "ha", "le"}, + }, + Embed: &EmbedSrc{ + SimpleSrc: SimpleSrc{ + Name: "xiaopeng", + Age: ekit.ToPtr[int](88), + Friends: []string{"la", "ha", "le"}, + }, + BasicSrc: &BasicSrc{ + Name: "wang", + Age: 22, + CNumber: complex(2, 1), + }, + }, + BasicSrc: BasicSrc{ + Name: "wang11", + Age: 22, + CNumber: complex(2, 1), + }, + }, IgnoreFields("Age")) + }, + wantDst: &ComplexDst{ + Simple: SimpleDst{ + Name: "xiaohong", + Age: nil, + Friends: []string{"ha", "ha", "le"}, + }, + Embed: &EmbedDst{ + SimpleSrc: SimpleSrc{ + Name: "xiaopeng", + Age: nil, + Friends: []string{"la", "ha", "le"}, + }, + BasicSrc: &BasicSrc{ + Name: "wang", + Age: 0, + CNumber: complex(2, 1), + }, + }, + BasicSrc: BasicSrc{ + Name: "wang11", + Age: 0, + CNumber: complex(2, 1), + }, + }, + }, + { + name: "复杂_Struct_忽略多层嵌套中的同名结构体", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ComplexSrc, ComplexDst]() + if err != nil { + return nil, err + } + return copier.Copy(&ComplexSrc{ + Simple: SimpleSrc{ + Name: "xiaohong", + Age: ekit.ToPtr[int](18), + Friends: []string{"ha", "ha", "le"}, + }, + Embed: &EmbedSrc{ + SimpleSrc: SimpleSrc{ + Name: "xiaopeng", + Age: ekit.ToPtr[int](88), + Friends: []string{"la", "ha", "le"}, + }, + BasicSrc: &BasicSrc{ + Name: "wang", + Age: 22, + CNumber: complex(2, 1), + }, + }, + BasicSrc: BasicSrc{ + Name: "wang11", + Age: 22, + CNumber: complex(2, 1), + }, + }, IgnoreFields("SimpleSrc")) + }, + wantDst: &ComplexDst{ + Simple: SimpleDst{ + Name: "xiaohong", + Age: ekit.ToPtr[int](18), + Friends: []string{"ha", "ha", "le"}, + }, + Embed: &EmbedDst{ + SimpleSrc: SimpleSrc{}, + BasicSrc: &BasicSrc{ + Name: "wang", + Age: 22, + CNumber: complex(2, 1), + }, + }, + BasicSrc: BasicSrc{ + Name: "wang11", + Age: 22, + CNumber: complex(2, 1), + }, + }, + }, + { + name: "复杂_Struct_忽略多层嵌套中的整个结构体", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ComplexSrc, ComplexDst]() + if err != nil { + return nil, err + } + return copier.Copy(&ComplexSrc{ + Simple: SimpleSrc{ + Name: "xiaohong", + Age: ekit.ToPtr[int](18), + Friends: []string{"ha", "ha", "le"}, + }, + Embed: &EmbedSrc{ + SimpleSrc: SimpleSrc{ + Name: "xiaopeng", + Age: ekit.ToPtr[int](88), + Friends: []string{"la", "ha", "le"}, + }, + BasicSrc: &BasicSrc{ + Name: "wang", + Age: 22, + CNumber: complex(2, 1), + }, + }, + BasicSrc: BasicSrc{ + Name: "wang11", + Age: 22, + CNumber: complex(2, 1), + }, + }, IgnoreFields("Embed")) + }, + wantDst: &ComplexDst{ + Simple: SimpleDst{ + Name: "xiaohong", + Age: ekit.ToPtr[int](18), + Friends: []string{"ha", "ha", "le"}, + }, + Embed: nil, + BasicSrc: BasicSrc{ + Name: "wang11", + Age: 22, + CNumber: complex(2, 1), + }, + }, + }, + { + name: "特殊类型_忽略结构体中的切片", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SpecialSrc, SpecialDst]() + if err != nil { + return nil, err + } + return copier.Copy(&SpecialSrc{ + Arr: [3]float32{1, 2, 3}, + M: map[string]int{ + "ha": 1, + "o": 2, + }, + }, IgnoreFields("Arr")) + }, + wantDst: &SpecialDst{ + Arr: [3]float32{}, + M: map[string]int{ + "ha": 1, + "o": 2, + }, + }, + }, + { + name: "特殊类型_忽略结构体中的map", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SpecialSrc, SpecialDst]() + if err != nil { + return nil, err + } + return copier.Copy(&SpecialSrc{ + Arr: [3]float32{1, 2, 3}, + M: map[string]int{ + "ha": 1, + "o": 2, + }, + }, IgnoreFields("M")) + }, + wantDst: &SpecialDst{ + Arr: [3]float32{1, 2, 3}, + M: nil, + }, + }, + { + name: "dst_有额外字段_忽略一个字段_其他字段会被赋值", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[DiffSrc, DiffDst]() + if err != nil { + return nil, err + } + dst := &DiffDst{ + A: "66", + B: 1, + d: SimpleSrc{ + Name: "wodemingzi", + Age: ekit.ToPtr(int(10)), + }, + G: BasicSrc{ + Name: "nidemingzi", + Age: 23, + CNumber: complex(1, 2), + }, + } + err = copier.CopyTo(&DiffSrc{ + A: "xiaowang", + B: 100, + c: SimpleSrc{ + Name: "66", + Age: ekit.ToPtr[int](100), + }, + F: BasicSrc{ + Name: "good name", + Age: 200, + CNumber: complex(2, 2), + }, + }, dst, IgnoreFields("A")) + return dst, err + }, + wantDst: &DiffDst{ + A: "66", + B: 100, + d: SimpleSrc{ + Name: "wodemingzi", + Age: ekit.ToPtr(int(10)), + }, + G: BasicSrc{ + Name: "nidemingzi", + Age: 23, + CNumber: complex(1, 2), + }, + }, + }, + { + name: "dst_有额外字段_不会忽略dst的字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[DiffSrc, DiffDst]() + if err != nil { + return nil, err + } + dst := &DiffDst{ + A: "66", + B: 1, + d: SimpleSrc{ + Name: "wodemingzi", + Age: ekit.ToPtr(int(10)), + }, + G: BasicSrc{ + Name: "nidemingzi", + Age: 23, + CNumber: complex(1, 2), + }, + } + err = copier.CopyTo(&DiffSrc{ + A: "xiaowang", + B: 100, + c: SimpleSrc{ + Name: "66", + Age: ekit.ToPtr[int](100), + }, + F: BasicSrc{ + Name: "good name", + Age: 200, + CNumber: complex(2, 2), + }, + }, dst, IgnoreFields("G")) + return dst, err + }, + wantDst: &DiffDst{ + A: "xiaowang", + B: 100, + d: SimpleSrc{ + Name: "wodemingzi", + Age: ekit.ToPtr(int(10)), + }, + G: BasicSrc{ + Name: "nidemingzi", + Age: 23, + CNumber: complex(1, 2), + }, + }, + }, + { + name: "成员为结构体数组_不会忽略结构体中的字段", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ArraySrc, ArrayDst]() + if err != nil { + return nil, err + } + return copier.Copy(&ArraySrc{ + A: []SimpleSrc{ + { + Name: "大明", + Age: ekit.ToPtr[int](18), + Friends: []string{"Tom", "Jerry"}, + }, + { + Name: "小明", + Age: ekit.ToPtr[int](8), + Friends: []string{"Tom"}, + }, + }, + }, IgnoreFields("Age")) + }, + wantDst: &ArrayDst{ + A: []SimpleSrc{ + { + Name: "大明", + Age: ekit.ToPtr[int](18), + Friends: []string{"Tom", "Jerry"}, + }, + { + Name: "小明", + Age: ekit.ToPtr[int](8), + Friends: []string{"Tom"}, + }, + }, + }, + }, } for _, tc := range testCases { From 45a0228fab76db887fc58386e099d185a82032e7 Mon Sep 17 00:00:00 2001 From: Longyue Li Date: Fri, 14 Jul 2023 15:01:05 +0800 Subject: [PATCH 16/32] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20syncx:=20map=20=20L?= =?UTF-8?q?oadOrStoreFunc=20(#198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: LoadOrStoreFunc直接调用m.LoadOrStore Signed-off-by: longyue0521 * refactor: 重构测试代码——使用子测试增强可理解性,用same方法代替equal方法明确意图“ Signed-off-by: longyue0521 * 添加.CHANGELOG.md Signed-off-by: longyue0521 --------- Signed-off-by: longyue0521 --- .CHANGELOG.md | 1 + syncx/map.go | 6 +- syncx/map_test.go | 250 +++++++++++++++++++++++++++++++--------------- 3 files changed, 170 insertions(+), 87 deletions(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index b08b6a30..3d48ca40 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -11,6 +11,7 @@ - [mapx: 为 MultipleMap 添加 PutVals 方法](https://github.com/ecodeclub/ekit/pull/189) - [mapx: LinkedMap 特性](https://github.com/ecodeclub/ekit/pull/191) - [copier: ReflectCopier 支持忽略字段](https://github.com/ecodeclub/ekit/pull/196) +- [syncx: 重构LoadOrStoreFunc方法及相关测试](https://github.com/ecodeclub/ekit/pull/198) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/syncx/map.go b/syncx/map.go index 428a5964..2c958784 100644 --- a/syncx/map.go +++ b/syncx/map.go @@ -56,7 +56,6 @@ func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { // 它的代价就是 Key 不存在的时候会多一次 Load 调用。 // 当 fn 返回 error 的时候,LoadOrStoreFunc 也会返回 error。 func (m *Map[K, V]) LoadOrStoreFunc(key K, fn func() (V, error)) (actual V, loaded bool, err error) { - var anyVal any val, ok := m.Load(key) if ok { return val, true, nil @@ -65,10 +64,7 @@ func (m *Map[K, V]) LoadOrStoreFunc(key K, fn func() (V, error)) (actual V, load if err != nil { return } - anyVal, loaded = m.m.LoadOrStore(key, val) - if anyVal != nil { - actual = anyVal.(V) - } + actual, loaded = m.LoadOrStore(key, val) return } diff --git a/syncx/map_test.go b/syncx/map_test.go index 73063a0b..95634e34 100644 --- a/syncx/map_test.go +++ b/syncx/map_test.go @@ -57,127 +57,213 @@ func TestMap_Load(t *testing.T) { }, } var mu Map[string, *User] - mu.Store("found", &User{Name: "found"}) - mu.Store("found but empty", &User{}) + mu.Store("found", testCases[0].wantVal) + mu.Store("found but empty", testCases[1].wantVal) mu.Store("found but nil", nil) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { val, ok := mu.Load(tc.key) assert.Equal(t, tc.wantOk, ok) - assert.Equal(t, tc.wantVal, val) + assert.Same(t, tc.wantVal, val) }) } } func TestMap_LoadOrStore(t *testing.T) { - var m = Map[string, *User]{} - val, loaded := m.LoadOrStore("Tom", &User{Name: "Tom"}) - assert.False(t, loaded) - assert.Equal(t, &User{Name: "Tom"}, val) - val, loaded = m.LoadOrStore("Tom", &User{Name: "Tom-copy"}) - assert.True(t, loaded) - assert.Equal(t, &User{Name: "Tom"}, val) + t.Run("store non-nil value", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Tom"} + val, loaded := m.LoadOrStore(user.Name, user) + assert.False(t, loaded) + assert.Same(t, user, val) + }) - val, loaded = m.LoadOrStore("Jerry", nil) - assert.False(t, loaded) - assert.Nil(t, val) + t.Run("load non-nil value", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Tom"} + val, loaded := m.LoadOrStore(user.Name, user) + assert.False(t, loaded) + assert.Same(t, user, val) - val, loaded = m.LoadOrStore("Jerry", &User{Name: "Jerry"}) - assert.True(t, loaded) - assert.Nil(t, val) + val, loaded = m.LoadOrStore("Tom", &User{Name: "Tom-copy"}) + + assert.True(t, loaded) + assert.Same(t, user, val) + }) + + t.Run("store nil value", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Jerry"} + val, loaded := m.LoadOrStore(user.Name, nil) + assert.False(t, loaded) + assert.Nil(t, val) + }) + + t.Run("load nil value", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Jerry"} + val, loaded := m.LoadOrStore(user.Name, nil) + assert.False(t, loaded) + assert.Nil(t, val) + + val, loaded = m.LoadOrStore(user.Name, user) + + assert.True(t, loaded) + assert.Nil(t, val) + }) } func TestMap_LoadOrStoreFunc(t *testing.T) { - var m = Map[string, *User]{} - val, loaded, err := m.LoadOrStoreFunc("Tom", func() (*User, error) { - return &User{Name: "Tom"}, nil + + t.Run("store non-nil value returned by func", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Tom"} + + val, loaded, err := m.LoadOrStoreFunc(user.Name, func() (*User, error) { + return user, nil + }) + + assert.NoError(t, err) + assert.False(t, loaded) + assert.Same(t, user, val) }) - assert.NoError(t, err) - assert.False(t, loaded) - assert.Equal(t, &User{Name: "Tom"}, val) - // 测试 Tom 存在的情况 - val, loaded, err = m.LoadOrStoreFunc("Tom", func() (*User, error) { - return &User{Name: "Tom"}, nil + t.Run("load non-nil value returned by func", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Tom"} + val, loaded, err := m.LoadOrStoreFunc(user.Name, func() (*User, error) { + return user, nil + }) + assert.NoError(t, err) + assert.False(t, loaded) + assert.Same(t, user, val) + + val, loaded, err = m.LoadOrStoreFunc(user.Name, func() (*User, error) { + return &User{Name: "Tom"}, nil + }) + + assert.NoError(t, err) + assert.True(t, loaded) + assert.Same(t, user, val) }) - assert.NoError(t, err) - assert.True(t, loaded) - assert.Equal(t, &User{Name: "Tom"}, val) - // 测试初始化失败 - val, loaded, err = m.LoadOrStoreFunc("Jerry", func() (*User, error) { - return nil, errors.New("初始话失败") + t.Run("store nil value returned by func", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Tom"} + + val, loaded, err := m.LoadOrStoreFunc(user.Name, func() (*User, error) { + return nil, nil + }) + + assert.NoError(t, err) + assert.False(t, loaded) + assert.Nil(t, val) + }) + + t.Run("load nil value returned by func", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Tom"} + val, loaded, err := m.LoadOrStoreFunc(user.Name, func() (*User, error) { + return nil, nil + }) + assert.NoError(t, err) + assert.False(t, loaded) + assert.Nil(t, val) + + val, loaded, err = m.LoadOrStoreFunc(user.Name, func() (*User, error) { + return nil, nil + }) + + assert.NoError(t, err) + assert.True(t, loaded) + assert.Nil(t, val) + }) + + t.Run("got error returned by func", func(t *testing.T) { + m := Map[string, *User]{} + val, loaded, err := m.LoadOrStoreFunc("Jerry", func() (*User, error) { + return nil, errors.New("初始话失败") + }) + assert.Equal(t, err, errors.New("初始话失败")) + assert.False(t, loaded) + assert.Equal(t, (*User)(nil), val) }) - assert.Equal(t, err, errors.New("初始话失败")) - assert.False(t, loaded) - assert.Equal(t, (*User)(nil), val) } func TestMap_LoadAndDelete(t *testing.T) { - var m = Map[string, *User]{} - m.Store("Tom", nil) - val, loaded := m.LoadAndDelete("Tom") - assert.True(t, loaded) - assert.Nil(t, val) - val, loaded = m.LoadAndDelete("Tom") - assert.False(t, loaded) - assert.Nil(t, val) + t.Run("non-nil value", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Jerry"} + m.Store("Jerry", user) - m.Store("Jerry", &User{Name: "Jerry"}) - val, loaded = m.LoadAndDelete("Jerry") - assert.True(t, loaded) - assert.Equal(t, &User{Name: "Jerry"}, val) + val, loaded := m.LoadAndDelete(user.Name) + assert.True(t, loaded) + assert.Same(t, user, val) - val, loaded = m.LoadAndDelete("Jerry") - assert.False(t, loaded) - assert.Nil(t, val) + val, loaded = m.LoadAndDelete(user.Name) + assert.False(t, loaded) + assert.Nil(t, val) + }) + + t.Run("nil value", func(t *testing.T) { + m, user := Map[string, *User]{}, &User{Name: "Tom"} + m.Store(user.Name, nil) + + val, loaded := m.LoadAndDelete(user.Name) + assert.True(t, loaded) + assert.Nil(t, val) + + val, loaded = m.LoadAndDelete(user.Name) + assert.False(t, loaded) + assert.Nil(t, val) + }) } func TestMap_Delete(t *testing.T) { - var m = Map[string, *User]{} - m.Store("Tom", &User{Name: "Tom"}) - val, ok := m.Load("Tom") + m, user := Map[string, *User]{}, &User{Name: "Tom"} + m.Store(user.Name, user) + val, ok := m.Load(user.Name) assert.True(t, ok) - assert.Equal(t, &User{Name: "Tom"}, val) - m.Delete("Tom") - val, ok = m.Load("Tom") + assert.Same(t, user, val) + + m.Delete(user.Name) + + val, ok = m.Load(user.Name) assert.False(t, ok) assert.Nil(t, val) } func TestMap_Range(t *testing.T) { - var m = Map[string, *User]{} - m.Store("Tom", &User{Name: "Tom"}) - m.Store("Jerry", &User{Name: "Jerry"}) - m.Store("nil", nil) - shadow := make(map[string]*User, 3) - m.Range(func(key string, val *User) bool { - shadow[key] = val - return true + t.Run("non-pointer type key", func(t *testing.T) { + m, tom, jerry := Map[string, *User]{}, &User{Name: "Tom"}, &User{Name: "Jerry"} + var zero *User + m.Store(tom.Name, tom) + m.Store(jerry.Name, jerry) + m.Store("zero", zero) + m.Store("nil", nil) + + shadow := make(map[string]*User, 4) + m.Range(func(key string, val *User) bool { + shadow[key] = val + return true + }) + + assert.Same(t, tom, shadow[tom.Name]) + assert.Same(t, jerry, shadow[jerry.Name]) + assert.Same(t, zero, shadow["zero"]) + assert.Same(t, (*User)(nil), shadow["nil"]) }) - assert.Equal(t, map[string]*User{ - "Tom": {Name: "Tom"}, - "Jerry": {Name: "Jerry"}, - "nil": nil, - }, shadow) - - var ptrKeyMap Map[*User, string] - key1 := &User{Name: "Tom"} - var key2 *User - ptrKeyMap.Store(key1, "Tom") - ptrKeyMap.Store(key2, "nil") - ptrShadow := make(map[*User]string, 2) - ptrKeyMap.Range(func(key *User, val string) bool { - ptrShadow[key] = val - return true + + t.Run("pointer type key", func(t *testing.T) { + m, tom := Map[*User, string]{}, &User{Name: "Tom"} + var zero *User + m.Store(tom, "Tom") + m.Store(zero, "nil") + + shadow := make(map[*User]string, 2) + m.Range(func(key *User, val string) bool { + shadow[key] = val + return true + }) + + assert.Equal(t, shadow[tom], tom.Name) + assert.Equal(t, shadow[zero], "nil") + assert.Equal(t, shadow[nil], "nil") }) - assert.Equal(t, map[*User]string{ - key1: "Tom", - nil: "nil", - }, ptrShadow) } func ExampleMap_LoadAndDelete() { From 4a29bd64f8603f79c3d4d66b053e72cc85c67982 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Thu, 20 Jul 2023 12:47:16 +0800 Subject: [PATCH 17/32] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BE=A4=E5=8F=B7=20(#?= =?UTF-8?q?199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .imgs/contact_me_qr.jpg | Bin 0 -> 87102 bytes README.md | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 .imgs/contact_me_qr.jpg diff --git a/.imgs/contact_me_qr.jpg b/.imgs/contact_me_qr.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e59f4463495738443c2e551f927524e7d9048f7 GIT binary patch literal 87102 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

IvK1V`lqX}jO<$jfB0pe1HR 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 Date: Wed, 2 Aug 2023 13:39:05 +0800 Subject: [PATCH 18/32] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B3=9B=E5=9E=8BAdd?= =?UTF-8?q?=E5=87=BD=E6=95=B0,=E5=9C=A8=E6=8C=87=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E6=8F=92=E5=85=A5=E5=85=83=E7=B4=A0=20(#201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 添加泛型Add函数,在指定位置插入元素 * 修改CHANGELOG文件 --- .CHANGELOG.md | 1 + internal/slice/add.go | 35 +++++++++++++++++ internal/slice/add_test.go | 78 ++++++++++++++++++++++++++++++++++++++ slice/add.go | 24 ++++++++++++ slice/add_test.go | 71 ++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+) create mode 100644 internal/slice/add.go create mode 100644 internal/slice/add_test.go create mode 100644 slice/add.go create mode 100644 slice/add_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 3d48ca40..14a849a6 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -12,6 +12,7 @@ - [mapx: LinkedMap 特性](https://github.com/ecodeclub/ekit/pull/191) - [copier: ReflectCopier 支持忽略字段](https://github.com/ecodeclub/ekit/pull/196) - [syncx: 重构LoadOrStoreFunc方法及相关测试](https://github.com/ecodeclub/ekit/pull/198) +- [slice: 添加Add函数,在指定位置插入元素](https://github.com/ecodeclub/ekit/pull/201) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/internal/slice/add.go b/internal/slice/add.go new file mode 100644 index 00000000..c368c50c --- /dev/null +++ b/internal/slice/add.go @@ -0,0 +1,35 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slice + +import "github.com/ecodeclub/ekit/internal/errs" + +func Add[T any](src []T, element T, index int) ([]T, error) { + length := len(src) + if index < 0 || index >= length { + return nil, errs.NewErrIndexOutOfRange(length, index) + } + + //先将src扩展一个元素 + var zeroValue T + src = append(src, zeroValue) + for i := len(src) - 1; i > index; i-- { + if i-1 >= 0 { + src[i] = src[i-1] + } + } + src[index] = element + return src, nil +} diff --git a/internal/slice/add_test.go b/internal/slice/add_test.go new file mode 100644 index 00000000..7236aece --- /dev/null +++ b/internal/slice/add_test.go @@ -0,0 +1,78 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slice + +import ( + "testing" + + "github.com/ecodeclub/ekit/internal/errs" + "github.com/stretchr/testify/assert" +) + +func TestAdd(t *testing.T) { + testCases := []struct { + name string + slice []int + addVal int + index int + wantSlice []int + wantErr error + }{ + { + name: "index 0", + slice: []int{123, 100}, + addVal: 233, + index: 0, + wantSlice: []int{233, 123, 100}, + }, + { + name: "index middle", + slice: []int{123, 124, 125}, + addVal: 233, + index: 1, + wantSlice: []int{123, 233, 124, 125}, + }, + { + name: "index out of range", + slice: []int{123, 100}, + index: 12, + wantErr: errs.NewErrIndexOutOfRange(2, 12), + }, + { + name: "index less than 0", + slice: []int{123, 100}, + index: -1, + wantErr: errs.NewErrIndexOutOfRange(2, -1), + }, + { + name: "index last", + slice: []int{123, 100, 101, 102, 102, 102}, + addVal: 233, + index: 5, + wantSlice: []int{123, 100, 101, 102, 102, 233, 102}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := Add(tc.slice, tc.addVal, tc.index) + assert.Equal(t, tc.wantErr, err) + if err != nil { + return + } + assert.Equal(t, tc.wantSlice, res) + }) + } +} diff --git a/slice/add.go b/slice/add.go new file mode 100644 index 00000000..a553a9f4 --- /dev/null +++ b/slice/add.go @@ -0,0 +1,24 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slice + +import "github.com/ecodeclub/ekit/internal/slice" + +// Add 在index处添加元素 +// index 范围应为[0, len(src)) +func Add[Src any](src []Src, element Src, index int) ([]Src, error) { + res, err := slice.Add[Src](src, element, index) + return res, err +} diff --git a/slice/add_test.go b/slice/add_test.go new file mode 100644 index 00000000..014a9df3 --- /dev/null +++ b/slice/add_test.go @@ -0,0 +1,71 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slice + +import ( + "fmt" + "testing" + + "github.com/ecodeclub/ekit/internal/errs" + + "github.com/stretchr/testify/assert" +) + +func TestAdd(t *testing.T) { + // Delete 主要依赖于 internal/slice.Delete 来保证正确性 + testCases := []struct { + name string + slice []int + addVal int + index int + wantSlice []int + wantErr error + }{ + { + name: "index 0", + slice: []int{123, 100}, + addVal: 233, + index: 0, + wantSlice: []int{233, 123, 100}, + }, + { + name: "index -1", + slice: []int{123, 100}, + index: -1, + wantErr: errs.NewErrIndexOutOfRange(2, -1), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := Add(tc.slice, tc.addVal, tc.index) + assert.Equal(t, tc.wantErr, err) + if err != nil { + return + } + assert.Equal(t, tc.wantSlice, res) + }) + } +} + +func ExampleAdd() { + res, _ := Add[int]([]int{1, 2, 3, 4}, 233, 2) + fmt.Println(res) + _, err := Add[int]([]int{1, 2, 3, 4}, 233, -1) + fmt.Println(err) + // Output: + // [1 2 233 3 4] + // ekit: 下标超出范围,长度 4, 下标 -1 +} From 5e15d8a4600cd8fcfba9e421606d0c21a21c622b Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Fri, 4 Aug 2023 14:59:52 +0800 Subject: [PATCH 19/32] =?UTF-8?q?mapx:=20=E6=94=AF=E6=8C=81=20builtinMap?= =?UTF-8?q?=EF=BC=8C=E7=94=A8=E4=BA=8E=E6=8E=A5=E5=85=A5=E5=85=B6=E5=AE=83?= =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8=E5=AE=9E=E7=8E=B0=20(#202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .CHANGELOG.md | 1 + mapx/builtin_map.go | 52 +++++ mapx/builtin_map_test.go | 215 +++++++++++++++++++ mapx/{multiMap.go => multi_map.go} | 7 + mapx/{multiMap_test.go => multi_map_test.go} | 27 +++ set/set.go | 2 +- 6 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 mapx/builtin_map.go create mode 100644 mapx/builtin_map_test.go rename mapx/{multiMap.go => multi_map.go} (93%) rename mapx/{multiMap_test.go => multi_map_test.go} (96%) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 14a849a6..e49469a0 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,6 +1,7 @@ # 开发中 - [mapx: TreeMap 添加 Keys 和 Values 方法](https://github.com/ecodeclub/ekit/pull/181) - [mapx: 修正 HashMap 中使用泛型不当的地方](https://github.com/ecodeclub/ekit/pull/186) +- [mapx: 支持 builtinMap,用于接入其它装饰器实现](https://github.com/ecodeclub/ekit/pull/202) - [pool: 重构TaskPool测试用例](https://github.com/ecodeclub/ekit/pull/178) - [sqlx:ScanRows 和 ScanAll方法](https://github.com/ecodeclub/ekit/pull/180) - [mapx: 修复红黑树删除节点问题](https://github.com/ecodeclub/ekit/pull/183) diff --git a/mapx/builtin_map.go b/mapx/builtin_map.go new file mode 100644 index 00000000..a5621d1e --- /dev/null +++ b/mapx/builtin_map.go @@ -0,0 +1,52 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapx + +// builtinMap 是对 map 的二次封装 +// 主要用于各种装饰器模式中被装饰的那个 +type builtinMap[K comparable, V any] struct { + data map[K]V +} + +func (b *builtinMap[K, V]) Put(key K, val V) error { + b.data[key] = val + return nil +} + +func (b *builtinMap[K, V]) Get(key K) (V, bool) { + val, ok := b.data[key] + return val, ok +} + +func (b *builtinMap[K, V]) Delete(k K) (V, bool) { + v, ok := b.data[k] + delete(b.data, k) + return v, ok +} + +// Keys 返回的 key 是随机的。即便对于同一个实例,调用两次,得到的结果都可能不同。 +func (b *builtinMap[K, V]) Keys() []K { + return Keys[K, V](b.data) +} + +func (b *builtinMap[K, V]) Values() []V { + return Values[K, V](b.data) +} + +func newBuiltinMap[K comparable, V any](capacity int) *builtinMap[K, V] { + return &builtinMap[K, V]{ + data: make(map[K]V, capacity), + } +} diff --git a/mapx/builtin_map_test.go b/mapx/builtin_map_test.go new file mode 100644 index 00000000..63572e66 --- /dev/null +++ b/mapx/builtin_map_test.go @@ -0,0 +1,215 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapx + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuiltinMap_Delete(t *testing.T) { + testCases := []struct { + name string + data map[string]string + + key string + + wantVal string + wantDeleted bool + }{ + { + name: "deleted", + data: map[string]string{ + "key1": "val1", + }, + key: "key1", + + wantVal: "val1", + wantDeleted: true, + }, + { + name: "key not exist", + data: map[string]string{ + "key1": "val1", + }, + key: "key2", + }, + { + name: "nil map", + key: "key2", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := builtinMapOf[string, string](tc.data) + val, ok := m.Delete(tc.key) + assert.Equal(t, tc.wantDeleted, ok) + assert.Equal(t, tc.wantVal, val) + _, ok = m.data[tc.key] + assert.False(t, ok) + }) + } +} + +func TestBuiltinMap_Get(t *testing.T) { + testCases := []struct { + name string + data map[string]string + + key string + + wantVal string + found bool + }{ + { + name: "found", + data: map[string]string{ + "key1": "val1", + }, + key: "key1", + + wantVal: "val1", + found: true, + }, + { + name: "key not exist", + data: map[string]string{ + "key1": "val1", + }, + key: "key2", + }, + { + name: "nil map", + key: "key2", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := builtinMapOf[string, string](tc.data) + val, ok := m.Get(tc.key) + assert.Equal(t, tc.found, ok) + assert.Equal(t, tc.wantVal, val) + }) + } +} + +func TestBuiltinMap_Put(t *testing.T) { + testCases := []struct { + name string + + key string + val string + cap int + + wantErr error + }{ + { + name: "puted", + key: "key1", + val: "val1", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := newBuiltinMap[string, string](tc.cap) + err := m.Put(tc.key, tc.val) + assert.Equal(t, tc.wantErr, err) + v, ok := m.data[tc.key] + assert.True(t, ok) + assert.Equal(t, tc.val, v) + }) + } +} + +func TestBuiltinMap_Keys(t *testing.T) { + testCases := []struct { + name string + data map[string]string + + wantKeys []string + }{ + { + name: "got keys", + data: map[string]string{ + "key1": "val1", + "key2": "val1", + "key3": "val1", + "key4": "val1", + }, + wantKeys: []string{"key1", "key2", "key3", "key4"}, + }, + { + name: "empty map", + data: map[string]string{}, + wantKeys: []string{}, + }, + { + name: "nil map", + wantKeys: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := builtinMapOf[string, string](tc.data) + keys := m.Keys() + assert.ElementsMatch(t, tc.wantKeys, keys) + }) + } +} + +func TestBuiltinMap_Values(t *testing.T) { + testCases := []struct { + name string + data map[string]string + + wantValues []string + }{ + { + name: "got values", + data: map[string]string{ + "key1": "val1", + "key2": "val2", + "key3": "val3", + "key4": "val4", + }, + wantValues: []string{"val1", "val2", "val3", "val4"}, + }, + { + name: "empty map", + data: map[string]string{}, + wantValues: []string{}, + }, + { + name: "nil map", + wantValues: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := builtinMapOf[string, string](tc.data) + vals := m.Values() + assert.ElementsMatch(t, tc.wantValues, vals) + }) + } +} + +func builtinMapOf[K comparable, V any](data map[K]V) *builtinMap[K, V] { + return &builtinMap[K, V]{data: data} +} diff --git a/mapx/multiMap.go b/mapx/multi_map.go similarity index 93% rename from mapx/multiMap.go rename to mapx/multi_map.go index 2a3fcbec..f6497164 100644 --- a/mapx/multiMap.go +++ b/mapx/multi_map.go @@ -45,6 +45,13 @@ func NewMultiHashMap[K Hashable, V any](size int) *MultiMap[K, V] { } } +func NewMultiBuiltinMap[K comparable, V any](size int) *MultiMap[K, V] { + var m mapi[K, []V] = newBuiltinMap[K, []V](size) + return &MultiMap[K, V]{ + m: m, + } +} + // Put 在 MultiMap 中添加键值对或向已有键 k 的值追加数据 func (m *MultiMap[K, V]) Put(k K, v V) error { return m.PutMany(k, v) diff --git a/mapx/multiMap_test.go b/mapx/multi_map_test.go similarity index 96% rename from mapx/multiMap_test.go rename to mapx/multi_map_test.go index 754e6327..0875763a 100644 --- a/mapx/multiMap_test.go +++ b/mapx/multi_map_test.go @@ -90,6 +90,33 @@ func TestMultiMap_NewMultiTreeMap(t *testing.T) { } } +func TestNewMultiBuiltinMap(t *testing.T) { + testCases := []struct { + name string + size int + }{ + { + name: "negative size", + size: -1, + }, + { + name: "zero size", + size: 0, + }, + { + name: "Positive size", + size: 1, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + multiMap := NewMultiBuiltinMap[testData, int](tt.size) + assert.NotNil(t, multiMap) + }) + } +} + func TestMultiMap_Keys(t *testing.T) { testCases := []struct { name string diff --git a/set/set.go b/set/set.go index dee45070..b1d194b3 100644 --- a/set/set.go +++ b/set/set.go @@ -16,8 +16,8 @@ package set type Set[T comparable] interface { Add(key T) - // 返回是否存在这个元素 Delete(key T) + // Exist 返回是否存在这个元素 Exist(key T) bool Keys() []T } From dd62bfc70ab4317958fa338b30be508a83fdfd73 Mon Sep 17 00:00:00 2001 From: Zhang Cancan <102995829+cancan927@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:40:41 +0800 Subject: [PATCH 20/32] =?UTF-8?q?=E4=BC=98=E5=8C=96slice=E7=9A=84delete?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E7=9A=84=E6=80=A7=E8=83=BD=20(#203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 优化slice的delete方法的性能 --- .CHANGELOG.md | 1 + internal/slice/delete.go | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index e49469a0..f38337b5 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -14,6 +14,7 @@ - [copier: ReflectCopier 支持忽略字段](https://github.com/ecodeclub/ekit/pull/196) - [syncx: 重构LoadOrStoreFunc方法及相关测试](https://github.com/ecodeclub/ekit/pull/198) - [slice: 添加Add函数,在指定位置插入元素](https://github.com/ecodeclub/ekit/pull/201) +- [slice: 优化delete方法,无需从头开始遍历](https://github.com/ecodeclub/ekit/pull/203) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/internal/slice/delete.go b/internal/slice/delete.go index 75468b1a..8a011c76 100644 --- a/internal/slice/delete.go +++ b/internal/slice/delete.go @@ -22,14 +22,12 @@ func Delete[T any](src []T, index int) ([]T, T, error) { var zero T return nil, zero, errs.NewErrIndexOutOfRange(length, index) } - j := 0 res := src[index] - for i, v := range src { - if i != index { - src[j] = v - j++ - } + //从index位置开始,后面的元素依次往前挪1个位置 + for i := index; i+1 < length; i++ { + src[i] = src[i+1] } - src = src[:j] + //去掉最后一个重复元素 + src = src[:length-1] return src, res, nil } From b656686505b38c56a11e6920249e9e20a982b2e1 Mon Sep 17 00:00:00 2001 From: hookokoko Date: Thu, 10 Aug 2023 22:37:54 +0800 Subject: [PATCH 21/32] =?UTF-8?q?copier=E6=94=AF=E6=8C=81=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E8=BD=AC=E6=8D=A2=20(#200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * copier支持类型转换 * 根据review的一些修改 * 修改atomicTypes的获取方式 * 解决并发copy的问题 & 调整文件位置 * 格式化代码 & 整合defaultOptions复制 --- .CHANGELOG.md | 1 + bean/copier/converter/converter.go | 25 ++ bean/copier/converter/time2string.go | 25 ++ bean/copier/copy.go | 28 +- bean/copier/errors.go | 5 + bean/copier/reflect_copier.go | 149 ++++++++--- bean/copier/reflect_copier_test.go | 366 ++++++++++++++++++++++++++- 7 files changed, 562 insertions(+), 37 deletions(-) create mode 100644 bean/copier/converter/converter.go create mode 100644 bean/copier/converter/time2string.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index f38337b5..e3924a8a 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,4 +1,5 @@ # 开发中 +- [copier: ReflectCopier copier支持类型转换](https://github.com/ecodeclub/ekit/issues/197) - [mapx: TreeMap 添加 Keys 和 Values 方法](https://github.com/ecodeclub/ekit/pull/181) - [mapx: 修正 HashMap 中使用泛型不当的地方](https://github.com/ecodeclub/ekit/pull/186) - [mapx: 支持 builtinMap,用于接入其它装饰器实现](https://github.com/ecodeclub/ekit/pull/202) diff --git a/bean/copier/converter/converter.go b/bean/copier/converter/converter.go new file mode 100644 index 00000000..08becfad --- /dev/null +++ b/bean/copier/converter/converter.go @@ -0,0 +1,25 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package converter + +type Converter[Src any, Dst any] interface { + Convert(src Src) (Dst, error) +} + +type ConverterFunc[Src any, Dst any] func(src Src) (Dst, error) + +func (cf ConverterFunc[Src, Dst]) Convert(src Src) (Dst, error) { + return cf(src) +} diff --git a/bean/copier/converter/time2string.go b/bean/copier/converter/time2string.go new file mode 100644 index 00000000..a2d25b4f --- /dev/null +++ b/bean/copier/converter/time2string.go @@ -0,0 +1,25 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package converter + +import "time" + +type Time2String struct { + Pattern string +} + +func (t Time2String) Convert(src time.Time) (string, error) { + return src.Format(t.Pattern), nil +} diff --git a/bean/copier/copy.go b/bean/copier/copy.go index df4c3f32..6ea8048b 100644 --- a/bean/copier/copy.go +++ b/bean/copier/copy.go @@ -15,6 +15,7 @@ package copier import ( + "github.com/ecodeclub/ekit/bean/copier/converter" "github.com/ecodeclub/ekit/bean/option" "github.com/ecodeclub/ekit/set" ) @@ -35,10 +36,14 @@ type Copier[Src any, Dst any] interface { type options struct { // ignoreFields 执行复制操作时,需要忽略的字段 ignoreFields *set.MapSet[string] + // convertFields 执行转换的field和转化接口的泛型包装 + convertFields map[string]converterWrapper } -func newOptions() *options { - return &options{} +type converterWrapper func(src any) (any, error) + +func newOptions() options { + return options{} } // InIgnoreFields 判断 str 是不是在 ignoreFields 里面 @@ -65,3 +70,22 @@ func IgnoreFields(fields ...string) option.Option[options] { } } } + +func ConvertField[Src any, Dst any](field string, converter converter.Converter[Src, Dst]) option.Option[options] { + return func(opt *options) { + if field == "" || converter == nil { + return + } + if opt.convertFields == nil { + opt.convertFields = make(map[string]converterWrapper, 8) + } + opt.convertFields[field] = func(src any) (any, error) { + var dst Dst + srcVal, ok := src.(Src) + if !ok { + return dst, errConvertFieldTypeNotMatch + } + return converter.Convert(srcVal) + } + } +} diff --git a/bean/copier/errors.go b/bean/copier/errors.go index fc9ce62b..b57149c5 100644 --- a/bean/copier/errors.go +++ b/bean/copier/errors.go @@ -15,10 +15,15 @@ package copier import ( + "errors" "fmt" "reflect" ) +var ( + errConvertFieldTypeNotMatch = errors.New("ekit: 转化字段类型不匹配") +) + // newErrTypeError copier 不支持的类型 func newErrTypeError(typ reflect.Type) error { return fmt.Errorf("ekit: copier 入口只支持 Struct 不支持类型 %v, 种类 %v", typ, typ.Kind()) diff --git a/bean/copier/reflect_copier.go b/bean/copier/reflect_copier.go index 28c7631c..aca46f31 100644 --- a/bean/copier/reflect_copier.go +++ b/bean/copier/reflect_copier.go @@ -16,10 +16,17 @@ package copier import ( "reflect" + "time" + + "github.com/ecodeclub/ekit/set" "github.com/ecodeclub/ekit/bean/option" ) +var defaultAtomicTypes = []reflect.Type{ + reflect.TypeOf(time.Time{}), +} + // ReflectCopier 基于反射的实现 // ReflectCopier 是浅拷贝 type ReflectCopier[Src any, Dst any] struct { @@ -28,7 +35,11 @@ type ReflectCopier[Src any, Dst any] struct { rootField fieldNode // options 执行复制操作时的可选配置 - options *options + // 如果默认配置和Copy()/CopyTo()中的配置同名,会替换defaultOptions同名内容 + // 初始化时的默认配置,仅作为记录,执行时会拷贝到options中 + defaultOptions options + + atomicTypes []reflect.Type } // fieldNode 字段的前缀树 @@ -50,7 +61,7 @@ type fieldNode struct { } // NewReflectCopier 如果类型不匹配, 创建时直接检查报错. -func NewReflectCopier[Src any, Dst any]() (*ReflectCopier[Src, Dst], error) { +func NewReflectCopier[Src any, Dst any](opts ...option.Option[options]) (*ReflectCopier[Src, Dst], error) { src := new(Src) srcTyp := reflect.TypeOf(src).Elem() dst := new(Dst) @@ -65,18 +76,24 @@ func NewReflectCopier[Src any, Dst any]() (*ReflectCopier[Src, Dst], error) { if dstTyp.Kind() != reflect.Struct { return nil, newErrTypeError(dstTyp) } - if err := createFieldNodes(&root, srcTyp, dstTyp); err != nil { - return nil, err - } copier := &ReflectCopier[Src, Dst]{ - rootField: root, + atomicTypes: defaultAtomicTypes, } + + if err := copier.createFieldNodes(&root, srcTyp, dstTyp); err != nil { + return nil, err + } + copier.rootField = root + + defaultOpts := newOptions() + option.Apply(&defaultOpts, opts...) + copier.defaultOptions = defaultOpts return copier, nil } // createFieldNodes 递归创建 field 的前缀树, srcTyp 和 dstTyp 只能是结构体 -func createFieldNodes(root *fieldNode, srcTyp, dstTyp reflect.Type) error { +func (r *ReflectCopier[Src, Dst]) createFieldNodes(root *fieldNode, srcTyp, dstTyp reflect.Type) error { fieldMap := map[string]int{} for i := 0; i < srcTyp.NumField(); i++ { @@ -98,17 +115,12 @@ func createFieldNodes(root *fieldNode, srcTyp, dstTyp reflect.Type) error { continue } srcFieldTypStruct := srcTyp.Field(srcIndex) - if srcFieldTypStruct.Type.Kind() != dstFieldTypStruct.Type.Kind() { - return newErrKindNotMatchError(srcFieldTypStruct.Type.Kind(), dstFieldTypStruct.Type.Kind(), dstFieldTypStruct.Name) - } - if srcFieldTypStruct.Type.Kind() == reflect.Pointer { - if srcFieldTypStruct.Type.Elem().Kind() != dstFieldTypStruct.Type.Elem().Kind() { - return newErrKindNotMatchError(srcFieldTypStruct.Type.Kind(), dstFieldTypStruct.Type.Kind(), dstFieldTypStruct.Name) - } - if srcFieldTypStruct.Type.Elem().Kind() == reflect.Pointer { - return newErrMultiPointer(dstFieldTypStruct.Name) - } + if srcFieldTypStruct.Type.Kind() == reflect.Pointer && srcFieldTypStruct.Type.Elem().Kind() == reflect.Pointer { + return newErrMultiPointer(srcFieldTypStruct.Name) + } + if dstFieldTypStruct.Type.Kind() == reflect.Pointer && dstFieldTypStruct.Type.Elem().Kind() == reflect.Pointer { + return newErrMultiPointer(dstFieldTypStruct.Name) } child := fieldNode{ @@ -123,18 +135,22 @@ func createFieldNodes(root *fieldNode, srcTyp, dstTyp reflect.Type) error { fieldDstTyp := dstFieldTypStruct.Type if fieldSrcTyp.Kind() == reflect.Pointer { fieldSrcTyp = fieldSrcTyp.Elem() + } + + if fieldDstTyp.Kind() == reflect.Pointer { fieldDstTyp = fieldDstTyp.Elem() } if isShadowCopyType(fieldSrcTyp.Kind()) { // 内置类型,但不匹配,如别名、map和slice - if fieldSrcTyp != fieldDstTyp { - return newErrTypeNotMatchError(srcFieldTypStruct.Type, dstFieldTypStruct.Type, dstFieldTypStruct.Name) - } // 说明当前节点是叶子节点, 直接拷贝 child.isLeaf = true + } else if r.isAtomicType(fieldSrcTyp) { + // 指定可作为一个整体的类型,不用递归 + // 同上,当当前节点是叶子节点时, 直接拷贝 + child.isLeaf = true } else if fieldSrcTyp.Kind() == reflect.Struct { - if err := createFieldNodes(&child, fieldSrcTyp, fieldDstTyp); err != nil { + if err := r.createFieldNodes(&child, fieldSrcTyp, fieldDstTyp); err != nil { return err } } else { @@ -160,41 +176,97 @@ func (r *ReflectCopier[Src, Dst]) Copy(src *Src, opts ...option.Option[options]) // 3. 如果 Src 和 Dst 中匹配的字段,其类型都是结构体,或者都是结构体指针,则会深入复制 // 4. 否则,忽略字段 func (r *ReflectCopier[Src, Dst]) CopyTo(src *Src, dst *Dst, opts ...option.Option[options]) error { - opt := newOptions() - option.Apply(opt, opts...) - r.options = opt + localOption := r.copyDefaultOptions() + option.Apply(&localOption, opts...) + return r.copyToWithTree(src, dst, localOption) +} - return r.copyToWithTree(src, dst) +// copyDefaultOptions 复制默认配置 +func (r *ReflectCopier[Src, Dst]) copyDefaultOptions() options { + localOption := newOptions() + // 复制ignoreFields default配置 + if r.defaultOptions.ignoreFields != nil { + ignoreFields := set.NewMapSet[string](8) + for _, key := range r.defaultOptions.ignoreFields.Keys() { + ignoreFields.Add(key) + } + localOption.ignoreFields = ignoreFields + } + + // 复制convertFields default配置 + for field, convert := range r.defaultOptions.convertFields { + if localOption.convertFields == nil { + localOption.convertFields = make(map[string]converterWrapper, 8) + } + localOption.convertFields[field] = convert + } + return localOption } -func (r *ReflectCopier[Src, Dst]) copyToWithTree(src *Src, dst *Dst) error { +func (r *ReflectCopier[Src, Dst]) copyToWithTree(src *Src, dst *Dst, opts options) error { srcTyp := reflect.TypeOf(src) dstTyp := reflect.TypeOf(dst) srcValue := reflect.ValueOf(src) dstValue := reflect.ValueOf(dst) - return r.copyTreeNode(srcTyp, srcValue, dstTyp, dstValue, &r.rootField) + return r.copyTreeNode(srcTyp, srcValue, dstTyp, dstValue, &r.rootField, opts) } -func (r *ReflectCopier[Src, Dst]) copyTreeNode(srcTyp reflect.Type, srcValue reflect.Value, dstType reflect.Type, dstValue reflect.Value, root *fieldNode) error { +func (r *ReflectCopier[Src, Dst]) copyTreeNode(srcTyp reflect.Type, srcValue reflect.Value, + dstType reflect.Type, dstValue reflect.Value, root *fieldNode, opts options) error { + originSrcVal := srcValue + originDstVal := dstValue if srcValue.Kind() == reflect.Pointer { if srcValue.IsNil() { return nil } - if dstValue.IsNil() { - dstValue.Set(reflect.New(dstType.Elem())) - } srcValue = srcValue.Elem() srcTyp = srcTyp.Elem() + } + if dstValue.Kind() == reflect.Pointer { + if dstValue.IsNil() { + dstValue.Set(reflect.New(dstType.Elem())) + } dstValue = dstValue.Elem() dstType = dstType.Elem() } + // 执行拷贝 if root.isLeaf { - if dstValue.CanSet() { + convert, ok := opts.convertFields[root.name] + if !dstValue.CanSet() { + return nil + } + // 获取convert失败,就需要检测类型是否匹配,类型匹配就直接set + if !ok { + if srcTyp != dstType { + return newErrTypeNotMatchError(srcTyp, dstType, root.name) + } + if srcValue.IsZero() { + return nil + } dstValue.Set(srcValue) + return nil } + + // 字段执行转换函数时,需要用到原始类型进行判断,set的时候也是根据原始value设置 + if !originDstVal.CanSet() { + return nil + } + srcConv, err := convert(originSrcVal.Interface()) + if err != nil { + return err + } + + srcConvType := reflect.TypeOf(srcConv) + srcConvVal := reflect.ValueOf(srcConv) + // 待设置的value和转换获取的value类型不匹配 + if srcConvType != originDstVal.Type() { + return newErrTypeNotMatchError(srcConvType, originDstVal.Type(), root.name) + } + + originDstVal.Set(srcConvVal) return nil } @@ -202,7 +274,7 @@ func (r *ReflectCopier[Src, Dst]) copyTreeNode(srcTyp reflect.Type, srcValue ref child := &root.fields[i] // 只要结构体属性的名字在需要忽略的字段里面,就不走下面的复制逻辑 - if r.options.InIgnoreFields(child.name) { + if opts.InIgnoreFields(child.name) { continue } @@ -211,13 +283,22 @@ func (r *ReflectCopier[Src, Dst]) copyTreeNode(srcTyp reflect.Type, srcValue ref childDstTyp := dstType.Field(child.dstIndex) childDstValue := dstValue.Field(child.dstIndex) - if err := r.copyTreeNode(childSrcTyp.Type, childSrcValue, childDstTyp.Type, childDstValue, child); err != nil { + if err := r.copyTreeNode(childSrcTyp.Type, childSrcValue, childDstTyp.Type, childDstValue, child, opts); err != nil { return err } } return nil } +func (r *ReflectCopier[Src, Dst]) isAtomicType(typ reflect.Type) bool { + for _, dt := range r.atomicTypes { + if dt == typ { + return true + } + } + return false +} + func isShadowCopyType(kind reflect.Kind) bool { switch kind { case reflect.Bool, diff --git a/bean/copier/reflect_copier_test.go b/bean/copier/reflect_copier_test.go index 131b9947..cef83977 100644 --- a/bean/copier/reflect_copier_test.go +++ b/bean/copier/reflect_copier_test.go @@ -15,14 +15,21 @@ package copier import ( + "fmt" "reflect" + "strconv" + "sync" "testing" + "time" + + "github.com/ecodeclub/ekit/bean/copier/converter" "github.com/ecodeclub/ekit" "github.com/stretchr/testify/assert" ) func TestReflectCopier_Copy(t *testing.T) { + t.Parallel() testCases := []struct { name string copyFunc func() (any, error) @@ -267,7 +274,7 @@ func TestReflectCopier_Copy(t *testing.T) { S: struct{ A string }{A: "a"}, }) }, - wantErr: newErrKindNotMatchError(reflect.String, reflect.Int, "A"), + wantErr: newErrTypeNotMatchError(reflect.TypeOf(""), reflect.TypeOf(0), "A"), }, { name: "多重指针", @@ -1040,6 +1047,308 @@ func TestReflectCopier_Copy(t *testing.T) { }, }, }, + { + name: "指定convert time2string,src为nil", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy(&ConvSimpleSrc{}, ConvertField[time.Time, string]("BirthDay", converter.Time2String{Pattern: "2006-01-02 15:04:05"})) + }, + wantDst: &ConvSimpleDst{ + BirthDay: "0001-01-01 00:00:00", + }, + }, + { + name: "指定convert time2string", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy(&ConvSimpleSrc{ + Name: "大明", + BirthDay: time.Date(2023, time.July, 26, 9, 15, 22, 213, time.UTC), + Friends: []string{"Tom", "Jerry"}, + }, ConvertField[time.Time, string]("BirthDay", converter.Time2String{Pattern: "2006-01-02 15:04:05"})) + }, + wantDst: &ConvSimpleDst{ + Name: "大明", + BirthDay: "2023-07-26 09:15:22", + Friends: []string{"Tom", "Jerry"}, + }, + }, + { + name: "指定convert func, src为nil", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy( + &ConvSimpleSrc{}, + ConvertField[string, string]( + "Name", + converter.ConverterFunc[string, string](func(src string) (string, error) { + newS := fmt.Sprintf("%s plus", src) + return newS, nil + }), + ), + ConvertField[time.Time, string]( + "BirthDay", + converter.ConverterFunc[time.Time, string](func(src time.Time) (string, error) { + return src.Format("2006-01-02 15:04:05"), nil + }), + ), + ConvertField[*int, *int]( + "Age", + converter.ConverterFunc[*int, *int](func(src *int) (*int, error) { + newS := *src + 1 + return &newS, nil + }), + ), + ConvertField[[]string, []string]( + "Friends", + converter.ConverterFunc[[]string, []string](func(src []string) ([]string, error) { + return []string{"Tom", "Jerry"}, nil + }), + ), + ) + }, + wantDst: &ConvSimpleDst{ + Name: " plus", + Age: nil, + BirthDay: "0001-01-01 00:00:00", + Friends: []string{"Tom", "Jerry"}, + }, + }, + { + name: "指定convert func, dst值为nil", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy( + &ConvSimpleSrc{ + Name: "大明", + Age: ekit.ToPtr[int](11), + BirthDay: time.Now(), + Friends: []string{"Tom", "Jerry"}, + }, + ConvertField[string, string]( + "Name", + converter.ConverterFunc[string, string](func(src string) (string, error) { + return "", nil + }), + ), + ConvertField[time.Time, string]( + "BirthDay", + converter.ConverterFunc[time.Time, string](func(src time.Time) (string, error) { + return "", nil + }), + ), + ConvertField[*int, *int]( + "Age", + converter.ConverterFunc[*int, *int](func(src *int) (*int, error) { + return nil, nil + }), + ), + ConvertField[[]string, []string]( + "Friends", + converter.ConverterFunc[[]string, []string](func(src []string) ([]string, error) { + return nil, nil + }), + ), + ) + }, + wantDst: &ConvSimpleDst{ + Name: "", + BirthDay: "", + Age: nil, + Friends: nil, + }, + }, + { + name: "指定convert func", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]() + if err != nil { + return nil, err + } + return copier.Copy( + &ConvSimpleSrc{ + Name: "大明", + Age: ekit.ToPtr[int](15), + BirthDay: time.Date(2023, time.July, 26, 9, 15, 22, 213, time.UTC), + Friends: []string{"Tom", "Jerry"}, + }, + ConvertField[string, string]( + "Name", + converter.ConverterFunc[string, string](func(src string) (string, error) { + newS := fmt.Sprintf("%s plus", src) + return newS, nil + }), + ), + ConvertField[time.Time, string]( + "BirthDay", + converter.ConverterFunc[time.Time, string](func(src time.Time) (string, error) { + return src.Format("2006-01-02 15:04:05"), nil + }), + ), + ConvertField[*int, *int]( + "Age", + converter.ConverterFunc[*int, *int](func(src *int) (*int, error) { + newS := *src + 1 + return &newS, nil + }), + ), + ) + }, + wantDst: &ConvSimpleDst{ + Name: "大明 plus", + Age: ekit.ToPtr[int](16), + BirthDay: "2023-07-26 09:15:22", + Friends: []string{"Tom", "Jerry"}, + }, + }, + { + name: "指定返回特殊类型的convert func", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSpecialSrc, ConvSpecialDst]() + if err != nil { + return nil, err + } + return copier.Copy(&ConvSpecialSrc{ + Arr: [3]float32{1, 2, 3}, + M: map[string]int{"a": 4, "b": 5, "c": 6}, + Diff: map[string]int{"a1": 41, "b1": 51, "c1": 61}, + }, ConvertField[map[string]int, map[string]int]( + "M", + converter.ConverterFunc[map[string]int, map[string]int](func(src map[string]int) (map[string]int, error) { + newM := map[string]int{"a1": 41, "b1": 51, "c1": 61} + return newM, nil + })), + ConvertField[map[string]int, []int]( + "Diff", + converter.ConverterFunc[map[string]int, []int](func(src map[string]int) ([]int, error) { + newM := []int{1, 1, 1} + return newM, nil + })), + ) + }, + wantDst: &ConvSpecialDst{ + Arr: [3]float32{1, 2, 3}, + M: map[string]int{"a1": 41, "b1": 51, "c1": 61}, + Diff: []int{1, 1, 1}, + }, + }, + { + name: "创建时指定默认converter", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]( + ConvertField[time.Time, string]( + "BirthDay", + converter.Time2String{Pattern: "2006-01-02 15:04:05"}, + ), + ) + if err != nil { + return nil, err + } + return copier.Copy(&ConvSimpleSrc{ + Name: "大明", + BirthDay: time.Date(2023, time.July, 26, 9, 15, 22, 213, time.UTC), + Friends: []string{"Tom", "Jerry"}, + }, ConvertField[string, string]("Name", converter.ConverterFunc[string, string](func(src string) (string, error) { + newS := fmt.Sprintf("%s plus", src) + return newS, nil + }))) + }, + wantDst: &ConvSimpleDst{ + Name: "大明 plus", + BirthDay: "2023-07-26 09:15:22", + Friends: []string{"Tom", "Jerry"}, + }, + }, + { + name: "创建时指定默认converter,convert同一个字段会覆盖", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]( + ConvertField[time.Time, string]( + "BirthDay", + converter.Time2String{Pattern: "2006-01-02 15:04:05"}, + ), + ) + if err != nil { + return nil, err + } + return copier.Copy(&ConvSimpleSrc{ + BirthDay: time.Date(2023, time.July, 26, 9, 15, 22, 213, time.UTC), + }, ConvertField[time.Time, string]("BirthDay", converter.ConverterFunc[time.Time, string](func(src time.Time) (string, error) { + return "1234567", nil + }))) + }, + wantDst: &ConvSimpleDst{ + BirthDay: "1234567", + }, + }, + { + name: "创建时指定默认converter,convert同一个字段会覆盖,覆盖后不影响默认配置", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]( + ConvertField[time.Time, string]( + "BirthDay", + converter.Time2String{Pattern: "2006-01-02 15:04:05"}, + ), + ) + if err != nil { + return nil, err + } + // 第一次执行Copy,函数中指定converter + _, err = copier.Copy( + &ConvSimpleSrc{BirthDay: time.Date(2023, time.July, 26, 9, 15, 22, 213, time.UTC)}, + ConvertField[time.Time, string]( + "BirthDay", + converter.ConverterFunc[time.Time, string](func(src time.Time) (string, error) { + return "1234567", nil + }))) + if err != nil { + return nil, err + } + // 第二次执行Copy,函数中不指定converter,走默认 + return copier.Copy(&ConvSimpleSrc{ + BirthDay: time.Date(2023, time.July, 26, 9, 15, 22, 213, time.UTC), + }) + }, + wantDst: &ConvSimpleDst{ + BirthDay: "2023-07-26 09:15:22", + }, + }, + { + name: "创建时指定默认忽略字段,Copy()时指定的忽略字段不影响默认", + copyFunc: func() (any, error) { + copier, err := NewReflectCopier[SimpleSrc, SimpleDst](IgnoreFields("Age")) + if err != nil { + return nil, err + } + // 第一次执行Copy,函数中指定ignore字段 + _, err = copier.Copy(&SimpleSrc{ + Name: "大明", + Age: ekit.ToPtr[int](11), + }, IgnoreFields("Name")) + if err != nil { + return nil, err + } + // 第二次执行Copy,函数中不指定ignore字段,走默认 + return copier.Copy(&SimpleSrc{ + Name: "大明", + }) + }, + wantDst: &SimpleDst{ + Name: "大明", + }, + }, } for _, tc := range testCases { @@ -1054,6 +1363,35 @@ func TestReflectCopier_Copy(t *testing.T) { } } +func Test_Concurrency_Copy(t *testing.T) { + copier, err := NewReflectCopier[ConvSimpleSrc, ConvSimpleDst]( + ConvertField[time.Time, string]( + "BirthDay", + converter.Time2String{Pattern: "2006-01-02 15:04:05"}, + ), + ) + assert.Nil(t, err) + + var wg sync.WaitGroup + wg.Add(100) + for i := 0; i < 100; i++ { + go func(i int) { + defer wg.Done() + val := strconv.Itoa(i) + c, err := copier.Copy( + &ConvSimpleSrc{BirthDay: time.Date(2023, time.July, 26, 9, 15, 22, 213, time.UTC)}, + ConvertField[time.Time, string]( + "BirthDay", + converter.ConverterFunc[time.Time, string](func(src time.Time) (string, error) { + return val, nil + }))) + assert.Nil(t, err) + assert.Equal(t, &ConvSimpleDst{BirthDay: val}, c) + }(i) + } + wg.Wait() +} + type BasicSrc struct { Name string Age int @@ -1201,6 +1539,32 @@ type SpecialDst2 struct { A aliasInt1 } +type ConvSimpleSrc struct { + Name string + Age *int + BirthDay time.Time + Friends []string +} + +type ConvSimpleDst struct { + Name string + Age *int + BirthDay string + Friends []string +} + +type ConvSpecialSrc struct { + Arr [3]float32 + M map[string]int + Diff map[string]int +} + +type ConvSpecialDst struct { + Arr [3]float32 + M map[string]int + Diff []int +} + func BenchmarkReflectCopier_Copy(b *testing.B) { // 复用 Copier b.Run("reused", func(b *testing.B) { From 5f533c9409d769f04d7f442dac82607c00189bc0 Mon Sep 17 00:00:00 2001 From: fifth-month <72730687+fifth-month@users.noreply.github.com> Date: Thu, 10 Aug 2023 22:49:21 +0800 Subject: [PATCH 22/32] =?UTF-8?q?syncx:=20sync.Cond=E7=9A=84=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E7=AD=89=E5=BE=85=E7=89=88=EF=BC=8CCond.WaitWithConte?= =?UTF-8?q?xt(ctx)=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Signed-off-by: Ming Deng Co-authored-by: Ming Deng --- .CHANGELOG.md | 1 + syncx/cond.go | 265 ++++++++++++++++++++++++++++++++ syncx/cond_sdk_test.go | 335 +++++++++++++++++++++++++++++++++++++++++ syncx/cond_test.go | 297 ++++++++++++++++++++++++++++++++++++ 4 files changed, 898 insertions(+) create mode 100644 syncx/cond.go create mode 100644 syncx/cond_sdk_test.go create mode 100644 syncx/cond_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index e3924a8a..ffa7d259 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,4 +1,5 @@ # 开发中 +- [syncx: sync.Cond的超时等待版,Cond.WaitWithContext(ctx)](https://github.com/ecodeclub/ekit/pull/192) - [copier: ReflectCopier copier支持类型转换](https://github.com/ecodeclub/ekit/issues/197) - [mapx: TreeMap 添加 Keys 和 Values 方法](https://github.com/ecodeclub/ekit/pull/181) - [mapx: 修正 HashMap 中使用泛型不当的地方](https://github.com/ecodeclub/ekit/pull/186) diff --git a/syncx/cond.go b/syncx/cond.go new file mode 100644 index 00000000..0f13e62f --- /dev/null +++ b/syncx/cond.go @@ -0,0 +1,265 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syncx + +import ( + "context" + "sync" + "sync/atomic" + "unsafe" +) + +// Cond 实现了一个条件变量,是等待或宣布一个事件发生的goroutines的汇合点。 +// +// 在改变条件和调用Wait方法的时候,Cond 关联的锁对象 L (*Mutex 或者 *RWMutex)必须被加锁, +// +// 在Go内存模型的术语中,Cond 保证 Broadcast或Signal的调用 同步于 因此而解除阻塞的 Wait 之前。 +// +// 绝大多数简单用例, 最好使用 channels 而不是 Cond +// (Broadcast 对应于关闭一个 channel, Signal 对应于给一个 channel 发送消息). +type Cond struct { + noCopy noCopy + // L 在观察或改变条件时被加锁 + L sync.Locker + notifyList *notifyList + // 用于指向自身的指针,可以用于检测是否被复制使用 + checker unsafe.Pointer + // 用于初始化notifyList + once sync.Once +} + +// NewCond 返回 关联了 l 的新 Cond . +func NewCond(l sync.Locker) *Cond { + return &Cond{L: l} +} + +// Wait 自动解锁 c.L 并挂起当前调用的 goroutine. 在恢复执行之后 Wait 在返回前将加 c.L 锁成功. +// 和其它系统不一样, 除非调用 Broadcast 或 Signal 或者 ctx 超时了,否则 Wait 不会返回. +// +// 成功唤醒时, 返回 nil. 超时失败时, 返回ctx.Err(). +// 如果 ctx 超时了, Wait 可能依旧执行成功返回 nil. +// +// 在 Wait 第一次继续执行时,因为 c.L 没有加锁, 当 Wait 返回的时候,调用者通常不能假设条件是真的 +// 相反, caller 应该在循环中调用 Wait: +// +// c.L.Lock() +// for !condition() { +// if err := c.Wait(ctx); err != nil { +// // 超时唤醒了,并不是被正常唤醒的,可以做一些超时的处理 +// } +// } +// ... condition 满足了,do work ... +// c.L.Unlock() +func (c *Cond) Wait(ctx context.Context) error { + c.checkCopy() + c.checkFirstUse() + t := c.notifyList.add() // 解锁前,将等待的对象放入链表中 + c.L.Unlock() // 一定是在等待对象放入链表后再解锁,避免刚解锁就发生协程切换,执行了signal后,再换回来导致永远阻塞 + defer c.L.Lock() + return c.notifyList.wait(ctx, t) +} + +// Signal 唤醒一个等待在 c 上的goroutine. +// +// 调用时,caller 可以持有也可以不持有 c.L 锁 +// +// Signal() 不影响 goroutine 调度的优先级; 如果其它的 goroutines +// 尝试着锁定 c.L, 它们可能在 "waiting" goroutine 之前被唤醒. +func (c *Cond) Signal() { + c.checkCopy() + c.checkFirstUse() + c.notifyList.notifyOne() +} + +// Broadcast 唤醒所有等待在 c 上的goroutine. +// +// 调用时,caller 可以持有也可以不持有 c.L 锁 +func (c *Cond) Broadcast() { + c.checkCopy() + c.checkFirstUse() + c.notifyList.notifyAll() +} + +// checkCopy 检查是否被拷贝使用 +func (c *Cond) checkCopy() { + // 判断checker保存的指针是否等于当前的指针(初始化时,并没有初始化checker的值,所以也会出现不相等) + if c.checker != unsafe.Pointer(c) && + // 由于初次初始化时,c.checker为0值,所以顺便进行一次原子替换,辅助初始化 + !atomic.CompareAndSwapPointer(&c.checker, nil, unsafe.Pointer(c)) && + // 再度检查checker保留指针是否等于当前指针 + c.checker != unsafe.Pointer(c) { + panic("syncx.Cond is copied") + } +} + +// checkFirstUse 用于初始化notifyList +func (c *Cond) checkFirstUse() { + c.once.Do(func() { + if c.notifyList == nil { + c.notifyList = newNotifyList() + } + }) +} + +// notifyList 是一个简单的 runtime_notifyList 实现,但增强了 wait 方法 +type notifyList struct { + mu sync.Mutex + list *chanList +} + +func newNotifyList() *notifyList { + return ¬ifyList{ + mu: sync.Mutex{}, + list: newChanList(), + } +} + +func (l *notifyList) add() *node { + l.mu.Lock() + defer l.mu.Unlock() + el := l.list.alloc() + l.list.pushBack(el) + return el +} + +func (l *notifyList) wait(ctx context.Context, elem *node) error { + ch := elem.Value + // 回收ch,超时时,因为没有被使用过,直接复用 + // 正常唤醒时,由于被放入了一条消息,但被取出来了一次,所以elem中的ch可以重复使用 + // 由于ch是挂在elem上的,所以elem在ch被回收之前,不可以被错误回收,所以必须在这里进行回收 + defer l.list.free(elem) + select { // 由于会随机选择一条,在超时和通知同时存在的话,如果通知先行,则没有影响,如果超时的同时,又来了通知 + case <-ctx.Done(): // 进了超时分支 + l.mu.Lock() + defer l.mu.Unlock() + select { + // double check: 检查是否在加锁前,刚好被正常通知了, + // 如果取到数据,代表收到了信号了,ch也因为被取了一次消息,意味着可以再次复用 + // 转移信号到下一个 + // 如果有下一个等待的,就唤醒它 + case <-ch: + if l.list.len() != 0 { + l.notifyNext() + } + // 如果取不到数据,代表不可能被正常唤醒了,ch也意味着没有被使用,可以从队列移除等待对象 + default: + l.list.remove(elem) + } + return ctx.Err() + case <-ch: // 如果取到数据,代表被正常唤醒了,ch也因为被取了一次消息,意味着可以再次复用 + return nil + } +} + +func (l *notifyList) notifyOne() { + l.mu.Lock() + defer l.mu.Unlock() + if l.list.len() == 0 { + return + } + l.notifyNext() +} + +func (l *notifyList) notifyNext() { + front := l.list.front() + ch := front.Value + l.list.remove(front) + ch <- struct{}{} +} + +func (l *notifyList) notifyAll() { + l.mu.Lock() + defer l.mu.Unlock() + for l.list.len() != 0 { + l.notifyNext() + } +} + +// node 保存chan的链表元素 +type node struct { + prev *node + next *node + Value chan struct{} +} + +// chanList 用于存放保存channel的一个双链表, 带复用元素的功能 +type chanList struct { + // 哨兵元素,方便处理元素个数为0的情况 + sentinel *node + size int + pool *sync.Pool +} + +func newChanList() *chanList { + sentinel := &node{} + sentinel.prev = sentinel + sentinel.next = sentinel + return &chanList{ + sentinel: sentinel, + size: 0, + pool: &sync.Pool{ + New: func() any { + return &node{ + Value: make(chan struct{}, 1), + } + }, + }, + } +} + +// len 获取链表长度 +func (l *chanList) len() int { + return l.size +} + +// front 获取队首元素 +func (l *chanList) front() *node { + return l.sentinel.next +} + +// alloc 申请新的元素,包含复用的chan +func (l *chanList) alloc() *node { + elem := l.pool.Get().(*node) + return elem +} + +// pushBack 追加元素到队尾 +func (l *chanList) pushBack(elem *node) { + elem.next = l.sentinel + elem.prev = l.sentinel.prev + l.sentinel.prev.next = elem + l.sentinel.prev = elem + l.size++ +} + +// remove 元素移除时,还不能回收该元素,避免元素上的chan被错误覆盖 +func (l *chanList) remove(elem *node) { + elem.prev.next = elem.next + elem.next.prev = elem.prev + elem.prev = nil + elem.next = nil + l.size-- +} + +// free 回收该元素,用于下次alloc获取时复用,避免再次分配 +func (l *chanList) free(elem *node) { + l.pool.Put(elem) +} + +// 用于静态代码检查复制的问题 +type noCopy struct{} + +func (*noCopy) Lock() {} +func (*noCopy) Unlock() {} diff --git a/syncx/cond_sdk_test.go b/syncx/cond_sdk_test.go new file mode 100644 index 00000000..9fb279ab --- /dev/null +++ b/syncx/cond_sdk_test.go @@ -0,0 +1,335 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was blatantly stolen from https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/sync/cond_test.go. + +package syncx + +import ( + "context" + "reflect" + "runtime" + "sync" + "testing" + "time" +) + +func TestCondSignal(t *testing.T) { + var m sync.Mutex + c := NewCond(&m) + n := 2 + running := make(chan bool, n) + awake := make(chan bool, n) + for i := 0; i < n; i++ { + go func() { + m.Lock() + running <- true + _ = c.Wait(context.Background()) + awake <- true + m.Unlock() + }() + } + for i := 0; i < n; i++ { + <-running // Wait for everyone to run. + } + for n > 0 { + select { + case <-awake: + t.Fatal("goroutine not asleep") + default: + } + m.Lock() + c.Signal() + m.Unlock() + <-awake // Will deadlock if no goroutine wakes up + select { + case <-awake: + t.Fatal("too many goroutines awake") + default: + } + n-- + } + c.Signal() +} + +func TestCondSignalGenerations(t *testing.T) { + var m sync.Mutex + c := NewCond(&m) + n := 100 + running := make(chan bool, n) + awake := make(chan int, n) + for i := 0; i < n; i++ { + go func(i int) { + m.Lock() + running <- true + _ = c.Wait(context.Background()) + awake <- i + m.Unlock() + }(i) + if i > 0 { + a := <-awake + if a != i-1 { + t.Fatalf("wrong goroutine woke up: want %d, got %d", i-1, a) + } + } + <-running + m.Lock() + c.Signal() + m.Unlock() + } +} + +func TestCondBroadcast(t *testing.T) { + var m sync.Mutex + c := NewCond(&m) + n := 200 + running := make(chan int, n) + awake := make(chan int, n) + exit := false + for i := 0; i < n; i++ { + go func(g int) { + m.Lock() + for !exit { + running <- g + _ = c.Wait(context.Background()) + awake <- g + } + m.Unlock() + }(i) + } + for i := 0; i < n; i++ { + for i := 0; i < n; i++ { + <-running // Will deadlock unless n are running. + } + if i == n-1 { + m.Lock() + exit = true + m.Unlock() + } + select { + case <-awake: + t.Fatal("goroutine not asleep") + default: + } + m.Lock() + c.Broadcast() + m.Unlock() + seen := make([]bool, n) + for i := 0; i < n; i++ { + g := <-awake + if seen[g] { + t.Fatal("goroutine woke up twice") + } + seen[g] = true + } + } + select { + case <-running: + t.Fatal("goroutine did not exit") + default: + } + c.Broadcast() +} + +func TestRace(t *testing.T) { + x := 0 + c := NewCond(&sync.Mutex{}) + done := make(chan bool) + go func() { + c.L.Lock() + x = 1 + _ = c.Wait(context.Background()) + if x != 2 { + t.Error("want 2") + } + x = 3 + c.Signal() + c.L.Unlock() + done <- true + }() + go func() { + c.L.Lock() + for { + if x == 1 { + x = 2 + c.Signal() + break + } + c.L.Unlock() + runtime.Gosched() + c.L.Lock() + } + c.L.Unlock() + done <- true + }() + go func() { + c.L.Lock() + for { + if x == 2 { + _ = c.Wait(context.Background()) + if x != 3 { + t.Error("want 3") + } + break + } + if x == 3 { + break + } + c.L.Unlock() + runtime.Gosched() + c.L.Lock() + } + c.L.Unlock() + done <- true + }() + <-done + <-done + <-done +} + +func TestCondSignalStealing(t *testing.T) { + for iters := 0; iters < 1000; iters++ { + var m sync.Mutex + cond := NewCond(&m) + + // Start a waiter. + ch := make(chan struct{}) + go func() { + m.Lock() + ch <- struct{}{} + _ = cond.Wait(context.Background()) + m.Unlock() + + ch <- struct{}{} + }() + + <-ch + m.Lock() + done := false + m.Unlock() + + // We know that the waiter is in the cond.Wait() call because we + // synchronized with it, then acquired/released the mutex it was + // holding when we synchronized. + // + // Start two goroutines that will race: one will broadcast on + // the cond var, the other will wait on it. + // + // The new waiter may or may not get notified, but the first one + // has to be notified. + + go func() { + cond.Broadcast() + }() + + go func() { + m.Lock() + for !done { + _ = cond.Wait(context.Background()) + } + m.Unlock() + }() + + // Check that the first waiter does get signaled. + select { + case <-ch: + case <-time.After(2 * time.Second): + t.Fatalf("First waiter didn't get broadcast.") + } + + // Release the second waiter in case it didn't get the + // broadcast. + m.Lock() + done = true + m.Unlock() + cond.Broadcast() + } +} + +func TestCondCopy(t *testing.T) { + defer func() { + err := recover() + if err == nil || err.(string) != "syncx.Cond is copied" { + t.Fatalf("got %v, expect syncx.Cond is copied", err) + } + }() + c := Cond{L: &sync.Mutex{}} + c.Signal() + var c2 Cond + reflect.ValueOf(&c2).Elem().Set(reflect.ValueOf(&c).Elem()) // c2 := c, hidden from vet + c2.Signal() +} + +func BenchmarkCond1(b *testing.B) { + benchmarkCond(b, 1) +} + +func BenchmarkCond2(b *testing.B) { + benchmarkCond(b, 2) +} + +func BenchmarkCond4(b *testing.B) { + benchmarkCond(b, 4) +} + +func BenchmarkCond8(b *testing.B) { + benchmarkCond(b, 8) +} + +func BenchmarkCond16(b *testing.B) { + benchmarkCond(b, 16) +} + +// BenchmarkCond32 test.bench: 100000x 31851 ns/op 1539 B/op 32 allocs/op +func BenchmarkCond32(b *testing.B) { + benchmarkCond(b, 32) +} + +func benchmarkCond(b *testing.B, waiters int) { + c := NewCond(&sync.Mutex{}) + done := make(chan bool) + id := 0 + + for routine := 0; routine < waiters+1; routine++ { + go func() { + for i := 0; i < b.N; i++ { + c.L.Lock() + if id == -1 { + c.L.Unlock() + break + } + id++ + if id == waiters+1 { + id = 0 + c.Broadcast() + } else { + _ = c.Wait(context.Background()) + } + c.L.Unlock() + } + c.L.Lock() + id = -1 + c.Broadcast() + c.L.Unlock() + done <- true + }() + } + for routine := 0; routine < waiters+1; routine++ { + <-done + } +} diff --git a/syncx/cond_test.go b/syncx/cond_test.go new file mode 100644 index 00000000..fb889d74 --- /dev/null +++ b/syncx/cond_test.go @@ -0,0 +1,297 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syncx + +import ( + "context" + "math/rand" + "reflect" + "sync" + "testing" + "time" +) + +func TestCond_Broadcast(t *testing.T) { + + cond := NewCond(&sync.Mutex{}) + + type status struct { + i int + err error + } + + sleepDuration := time.Millisecond * 100 + + var n = 100 + running := make(chan int, n) + awake := make(chan status, n) + waitSeqs := make([]int, n) + normalAwakeSeqs := make([]int, 0, n) + timeoutAwakeSeqs := make([]int, 0, n) + minTimeoutCnt := 0 + minNormalCnt := 0 + seen := make(map[int]bool, n) + for i := 0; i < n; i++ { + duration := time.Millisecond * 50 * time.Duration(rand.Int()%4+1) + if duration < sleepDuration*9/10 { + minTimeoutCnt++ + } else if duration > sleepDuration*11/10 { + minNormalCnt++ + } + go func(i int) { + cond.L.Lock() + + ctx, cancelFunc := context.WithTimeout(context.Background(), duration) + defer cancelFunc() + running <- i + err := cond.Wait(ctx) + awake <- status{ + i: i, + err: err, + } + cond.L.Unlock() + }(i) + } + for i := 0; i < n; i++ { + waitSeqs[i] = <-running + } + + time.Sleep(100 * time.Millisecond) + + cond.L.Lock() + cond.Broadcast() + cond.L.Unlock() + + for i := 0; i < n; i++ { + stat := <-awake + if seen[stat.i] { + t.Fatal("goroutine woke up twice") + } else { + seen[stat.i] = true + } + if stat.err != nil { + timeoutAwakeSeqs = append(timeoutAwakeSeqs, stat.i) + } else { + normalAwakeSeqs = append(normalAwakeSeqs, stat.i) + } + } + + if len(normalAwakeSeqs) < minNormalCnt { + t.Fatal("goroutine woke up with timeout") + } + + if len(timeoutAwakeSeqs) < minTimeoutCnt { + t.Fatal("goroutine woke up with normally") + } +} + +func TestCond_Signal(t *testing.T) { + + cond := NewCond(&sync.Mutex{}) + + type status struct { + i int + err error + } + + sleepDuration := time.Millisecond * 100 + + var n = 100 + running := make(chan int, n) + awake := make(chan status, n) + waitSeqs := make([]int, n) + normalAwakeSeqs := make([]int, 0, n) + timeoutAwakeSeqs := make([]int, 0, n) + minTimeoutCnt := 0 + minNormalCnt := 0 + seen := make(map[int]bool, n) + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + duration := time.Millisecond * 50 * time.Duration(rand.Int()%4+1) + if duration < sleepDuration*9/10 { + minTimeoutCnt++ + } else if duration > sleepDuration*11/10 { + minNormalCnt++ + } + go func(i int) { + cond.L.Lock() + + ctx, cancelFunc := context.WithTimeout(context.Background(), duration) + defer cancelFunc() + running <- i + err := cond.Wait(ctx) + awake <- status{ + i: i, + err: err, + } + cond.L.Unlock() + wg.Done() + }(i) + } + for i := 0; i < n; i++ { + waitSeqs[i] = <-running + } + + go func() { + wg.Wait() + close(awake) + }() + + time.Sleep(100 * time.Millisecond) + + for i := 0; i < n; i++ { + cond.L.Lock() + cond.Signal() + cond.L.Unlock() + for { + stat, ok := <-awake + if !ok { + break + } + if seen[stat.i] { + t.Fatal("goroutine woke up twice") + } else { + seen[stat.i] = true + } + if stat.err != nil { + timeoutAwakeSeqs = append(timeoutAwakeSeqs, stat.i) + } else { + normalAwakeSeqs = append(normalAwakeSeqs, stat.i) + break + } + } + + } + + if len(normalAwakeSeqs) < minNormalCnt { + t.Fatal("goroutine woke up with timeout") + } + + if len(timeoutAwakeSeqs) < minTimeoutCnt { + t.Fatal("goroutine woke up with normally") + } + // 测试singnal唤醒的顺序问题 + if !isInOrder(normalAwakeSeqs, waitSeqs) { + t.Fatal("goroutine woke up not in order") + } + // 超时唤醒的肯定是乱序的,没有好办法测试顺序 + //if !isInOrder(timeoutAwakeSeqs, waitSeqs) { + // t.Fatal("goroutine woke up not in order") + //} +} + +func isInOrder(partial []int, source []int) bool { + + j := 0 + + for i := 0; i < len(partial); i++ { + matched := false + for j < len(source) { + if partial[i] == source[j] { + j++ + matched = true + break + } + j++ + continue + } + if !matched { + return false + } + } + + return true +} + +func Test_InOrder(t *testing.T) { + testcases := []struct { + name string + partial []int + source []int + want bool + }{ + {"", []int{1}, []int{1}, true}, + {"", []int{1, 3, 4}, []int{1, 2, 3, 4}, true}, + {"", []int{1, 3}, []int{1, 2, 3, 4}, true}, + {"", []int{1, 3, 2}, []int{1, 2, 3, 4}, false}, + {"", []int{1, 2, 2}, []int{1, 2, 3, 4}, false}, + {"", []int{1, 2, 3}, []int{1, 3, 2, 4}, false}, + {"", []int{1, 2, 4}, []int{1, 3, 2, 4}, true}, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + if target := isInOrder(tt.partial, tt.source); target != tt.want { + t.Errorf("get %v, want %v", target, tt.want) + } + }) + } +} + +// TestChanList 测试有序,和清空后重复使用是否有问题 +func TestChanList(t *testing.T) { + + l := newChanList() + + testcases := []struct { + name string + num int + }{ + {"", 5}, + {"", 3}, + {"", 10}, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(tt *testing.T) { + inputNodes := make([]*node, 0, testcase.num) + inputChans := make([]chan struct{}, 0, testcase.num) + for i := 0; i < testcase.num; i++ { + ele := l.alloc() + inputNodes = append(inputNodes, ele) + inputChans = append(inputChans, ele.Value) + l.pushBack(ele) + } + if length := l.len(); length != testcase.num { + t.Errorf("list.len() = %v, want %v", length, testcase.num) + } + outNodes := make([]*node, 0, testcase.num) + outChans := make([]chan struct{}, 0, testcase.num) + for l.len() != 0 { + front := l.front() + outNodes = append(outNodes, front) + outChans = append(outChans, front.Value) + l.remove(front) + } + if !reflect.DeepEqual(outChans, inputChans) { + t.Errorf("chan list is %v, but got %v", inputChans, outChans) + } + if !reflect.DeepEqual(outNodes, inputNodes) { + t.Errorf("element list is %v, but got %v", inputNodes, outNodes) + } + }) + } +} + +// BenchmarkChanList 测试有无内存分配增加的情况 +func BenchmarkChanList(b *testing.B) { + l := newChanList() + for i := 0; i < b.N; i++ { + elem := l.alloc() + l.pushBack(elem) + l.remove(elem) + } +} From 4a09f173134282ded2e485dfb86fac175313f3a8 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Mon, 14 Aug 2023 16:26:35 +0800 Subject: [PATCH 23/32] =?UTF-8?q?slice:=20=E9=87=8D=E6=9E=84=20slice=20?= =?UTF-8?q?=E4=B8=AD=E4=BD=BF=E7=94=A8=20equalFunc=20=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95=20(#205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 引入 matchFunc 2. 简化 slice 中查找类型的 API 3. 引入 Find 和 FindAll 方法 --- .CHANGELOG.md | 1 + slice/contains.go | 10 +-- slice/contains_test.go | 8 +-- slice/diff.go | 5 +- slice/find.go | 43 ++++++++++++ slice/find_test.go | 149 ++++++++++++++++++++++++++++++++++++++++ slice/index.go | 22 +++--- slice/index_test.go | 28 ++++---- slice/map.go | 4 +- slice/symmetric_diff.go | 8 ++- slice/types.go | 2 + 11 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 slice/find.go create mode 100644 slice/find_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index ffa7d259..7e88479d 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -17,6 +17,7 @@ - [syncx: 重构LoadOrStoreFunc方法及相关测试](https://github.com/ecodeclub/ekit/pull/198) - [slice: 添加Add函数,在指定位置插入元素](https://github.com/ecodeclub/ekit/pull/201) - [slice: 优化delete方法,无需从头开始遍历](https://github.com/ecodeclub/ekit/pull/203) +- [slice: 重构 slice 中使用 equalFunc 的方法](https://github.com/ecodeclub/ekit/pull/205) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/slice/contains.go b/slice/contains.go index 2d378a9f..e7431dc6 100644 --- a/slice/contains.go +++ b/slice/contains.go @@ -16,17 +16,17 @@ package slice // Contains 判断 src 里面是否存在 dst func Contains[T comparable](src []T, dst T) bool { - return ContainsFunc[T](src, dst, func(src, dst T) bool { + return ContainsFunc[T](src, func(src T) bool { return src == dst }) } // ContainsFunc 判断 src 里面是否存在 dst // 你应该优先使用 Contains -func ContainsFunc[T any](src []T, dst T, equal equalFunc[T]) bool { +func ContainsFunc[T any](src []T, equal func(src T) bool) bool { // 遍历调用equal函数进行判断 for _, v := range src { - if equal(v, dst) { + if equal(v) { return true } } @@ -72,7 +72,9 @@ func ContainsAll[T comparable](src, dst []T) bool { // 你应该优先使用 ContainsAll func ContainsAllFunc[T any](src, dst []T, equal equalFunc[T]) bool { for _, valDst := range dst { - if !ContainsFunc[T](src, valDst, equal) { + if !ContainsFunc[T](src, func(src T) bool { + return equal(src, valDst) + }) { return false } } diff --git a/slice/contains_test.go b/slice/contains_test.go index 015841eb..43f2e2f5 100644 --- a/slice/contains_test.go +++ b/slice/contains_test.go @@ -92,8 +92,8 @@ func TestContainsFunc(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.want, ContainsFunc[int](test.src, test.dst, func(src, dst int) bool { - return src == dst + assert.Equal(t, test.want, ContainsFunc[int](test.src, func(src int) bool { + return src == test.dst })) }) } @@ -287,8 +287,8 @@ func ExampleContains() { } func ExampleContainsFunc() { - res := ContainsFunc[int]([]int{1, 2, 3}, 3, func(src, dst int) bool { - return src == dst + res := ContainsFunc[int]([]int{1, 2, 3}, func(src int) bool { + return src == 3 }) fmt.Println(res) // Output: diff --git a/slice/diff.go b/slice/diff.go index 7d9dea36..5d12d65f 100644 --- a/slice/diff.go +++ b/slice/diff.go @@ -34,10 +34,11 @@ func DiffSet[T comparable](src, dst []T) []T { // DiffSetFunc 差集,已去重 // 你应该优先使用 DiffSet func DiffSetFunc[T any](src, dst []T, equal equalFunc[T]) []T { - // TODO 优化容量预估 var ret = make([]T, 0, len(src)) for _, val := range src { - if !ContainsFunc[T](dst, val, equal) { + if !ContainsFunc[T](dst, func(src T) bool { + return equal(src, val) + }) { ret = append(ret, val) } } diff --git a/slice/find.go b/slice/find.go new file mode 100644 index 00000000..88677f68 --- /dev/null +++ b/slice/find.go @@ -0,0 +1,43 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slice + +// Find 查找元素 +// 如果没有找到,第二个返回值返回 false +func Find[T any](src []T, match matchFunc[T]) (T, bool) { + for _, val := range src { + if match(val) { + return val, true + } + } + var t T + return t, false +} + +// FindAll 查找所有符合条件的元素 +// 永远不会返回 nil +func FindAll[T any](src []T, match matchFunc[T]) []T { + // 我们认为符合条件元素应该是少数 + // 所以会除以 8 + // 也就是触发扩容的情况下,最多三次就会和原本的容量一样 + // +1 是为了保证,至少有一个元素 + res := make([]T, 0, len(src)>>3+1) + for _, val := range src { + if match(val) { + res = append(res, val) + } + } + return res +} diff --git a/slice/find_test.go b/slice/find_test.go new file mode 100644 index 00000000..90f2d30e --- /dev/null +++ b/slice/find_test.go @@ -0,0 +1,149 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slice + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFind(t *testing.T) { + testCases := []struct { + name string + input []Number + match matchFunc[Number] + + wantVal Number + found bool + }{ + { + name: "找到了", + input: []Number{ + {val: 123}, + {val: 234}, + }, + match: func(src Number) bool { + return src.val == 123 + }, + wantVal: Number{val: 123}, + found: true, + }, + { + name: "没找到", + input: []Number{ + {val: 123}, + {val: 234}, + }, + match: func(src Number) bool { + return src.val == 456 + }, + }, + { + name: "nil", + match: func(src Number) bool { + return src.val == 123 + }, + }, + { + name: "没有元素", + input: []Number{}, + match: func(src Number) bool { + return src.val == 123 + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + val, found := Find[Number](tc.input, tc.match) + assert.Equal(t, tc.found, found) + assert.Equal(t, tc.wantVal, val) + }) + } +} + +func TestFindAll(t *testing.T) { + testCases := []struct { + name string + input []Number + match matchFunc[Number] + + wantVals []Number + }{ + { + name: "没有符合条件的", + input: []Number{{val: 2}, {val: 4}}, + match: func(src Number) bool { + return src.val%2 == 1 + }, + wantVals: []Number{}, + }, + { + name: "找到了", + input: []Number{{val: 2}, {val: 3}, {val: 4}}, + match: func(src Number) bool { + return src.val%2 == 1 + }, + wantVals: []Number{{val: 3}}, + }, + { + name: "nil", + match: func(src Number) bool { + return src.val%2 == 1 + }, + wantVals: []Number{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + vals := FindAll[Number](tc.input, tc.match) + assert.Equal(t, tc.wantVals, vals) + }) + } +} + +func ExampleFind() { + val, ok := Find[int]([]int{1, 2, 3}, func(src int) bool { + return src == 2 + }) + fmt.Println(val, ok) + val, ok = Find[int]([]int{1, 2, 3}, func(src int) bool { + return src == 4 + }) + fmt.Println(val, ok) + // Output: + // 2 true + // 0 false +} + +func ExampleFindAll() { + vals := FindAll[int]([]int{2, 3, 4}, func(src int) bool { + return src%2 == 1 + }) + fmt.Println(vals) + vals = FindAll[int]([]int{2, 3, 4}, func(src int) bool { + return src > 5 + }) + fmt.Println(vals) + // Output: + // [3] + // [] +} + +type Number struct { + val int +} diff --git a/slice/index.go b/slice/index.go index 3da1034c..029bd49d 100644 --- a/slice/index.go +++ b/slice/index.go @@ -17,17 +17,17 @@ package slice // Index 返回和 dst 相等的第一个元素下标 // -1 表示没找到 func Index[T comparable](src []T, dst T) int { - return IndexFunc[T](src, dst, func(src, dst T) bool { + return IndexFunc[T](src, func(src T) bool { return src == dst }) } -// IndexFunc 返回和 dst 相等的第一个元素下标 +// IndexFunc 返回 match 返回 true 的第一个下标 // -1 表示没找到 // 你应该优先使用 Index -func IndexFunc[T any](src []T, dst T, equal equalFunc[T]) int { +func IndexFunc[T any](src []T, match matchFunc[T]) int { for k, v := range src { - if equal(v, dst) { + if match(v) { return k } } @@ -37,7 +37,7 @@ func IndexFunc[T any](src []T, dst T, equal equalFunc[T]) int { // LastIndex 返回和 dst 相等的最后一个元素下标 // -1 表示没找到 func LastIndex[T comparable](src []T, dst T) int { - return LastIndexFunc[T](src, dst, func(src, dst T) bool { + return LastIndexFunc[T](src, func(src T) bool { return src == dst }) } @@ -45,9 +45,9 @@ func LastIndex[T comparable](src []T, dst T) int { // LastIndexFunc 返回和 dst 相等的最后一个元素下标 // -1 表示没找到 // 你应该优先使用 LastIndex -func LastIndexFunc[T any](src []T, dst T, equal equalFunc[T]) int { +func LastIndexFunc[T any](src []T, match matchFunc[T]) int { for i := len(src) - 1; i >= 0; i-- { - if equal(dst, src[i]) { + if match(src[i]) { return i } } @@ -56,17 +56,17 @@ func LastIndexFunc[T any](src []T, dst T, equal equalFunc[T]) int { // IndexAll 返回和 dst 相等的所有元素的下标 func IndexAll[T comparable](src []T, dst T) []int { - return IndexAllFunc[T](src, dst, func(src, dst T) bool { + return IndexAllFunc[T](src, func(src T) bool { return src == dst }) } -// IndexAllFunc 返回和 dst 相等的所有元素的下标 +// IndexAllFunc 返回和 match 返回 true 的所有元素的下标 // 你应该优先使用 IndexAll -func IndexAllFunc[T any](src []T, dst T, equal equalFunc[T]) []int { +func IndexAllFunc[T any](src []T, match matchFunc[T]) []int { var indexes = make([]int, 0, len(src)) for k, v := range src { - if equal(v, dst) { + if match(v) { indexes = append(indexes, k) } } diff --git a/slice/index_test.go b/slice/index_test.go index 03c1199c..c29bdfaf 100644 --- a/slice/index_test.go +++ b/slice/index_test.go @@ -104,8 +104,8 @@ func TestIndexFunc(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.want, IndexFunc[int](test.src, test.dst, func(src, dst int) bool { - return src == dst + assert.Equal(t, test.want, IndexFunc[int](test.src, func(src int) bool { + return src == test.dst })) }) } @@ -191,8 +191,8 @@ func TestLastIndexFunc(t *testing.T) { }, } for _, test := range tests { - assert.Equal(t, test.want, LastIndexFunc[int](test.src, test.dst, func(src, dst int) bool { - return src == dst + assert.Equal(t, test.want, LastIndexFunc[int](test.src, func(src int) bool { + return src == test.dst })) } } @@ -268,8 +268,8 @@ func TestIndexAllFunc(t *testing.T) { }, } for _, test := range tests { - res := IndexAllFunc[int](test.src, test.dst, func(src, dst int) bool { - return src == dst + res := IndexAllFunc[int](test.src, func(src int) bool { + return src == test.dst }) assert.ElementsMatch(t, test.want, res) } @@ -286,12 +286,12 @@ func ExampleIndex() { } func ExampleIndexFunc() { - res := IndexFunc[int]([]int{1, 2, 3}, 1, func(src, dst int) bool { - return src == dst + res := IndexFunc[int]([]int{1, 2, 3}, func(src int) bool { + return src == 1 }) fmt.Println(res) - res = IndexFunc[int]([]int{1, 2, 3}, 4, func(src, dst int) bool { - return src == dst + res = IndexFunc[int]([]int{1, 2, 3}, func(src int) bool { + return src == 4 }) fmt.Println(res) // Output: @@ -310,12 +310,12 @@ func ExampleIndexAll() { } func ExampleIndexAllFunc() { - res := IndexAllFunc[int]([]int{1, 2, 3, 4, 5, 3, 9}, 3, func(src, dst int) bool { - return src == dst + res := IndexAllFunc[int]([]int{1, 2, 3, 4, 5, 3, 9}, func(src int) bool { + return src == 3 }) fmt.Println(res) - res = IndexAllFunc[int]([]int{1, 2, 3}, 4, func(src, dst int) bool { - return src == dst + res = IndexAllFunc[int]([]int{1, 2, 3}, func(src int) bool { + return src == 4 }) fmt.Println(res) // Output: diff --git a/slice/map.go b/slice/map.go index 7732de48..11ce496f 100644 --- a/slice/map.go +++ b/slice/map.go @@ -49,7 +49,9 @@ func toMap[T comparable](src []T) map[T]struct{} { func deduplicateFunc[T any](data []T, equal equalFunc[T]) []T { var newData = make([]T, 0, len(data)) for k, v := range data { - if !ContainsFunc[T](data[k+1:], v, equal) { + if !ContainsFunc[T](data[k+1:], func(src T) bool { + return equal(src, v) + }) { newData = append(newData, v) } } diff --git a/slice/symmetric_diff.go b/slice/symmetric_diff.go index 2b970cc4..d3bd1e9b 100644 --- a/slice/symmetric_diff.go +++ b/slice/symmetric_diff.go @@ -54,12 +54,16 @@ func SymmetricDiffSetFunc[T any](src, dst []T, equal equalFunc[T]) []T { ret := make([]T, 0, len(src)+len(dst)-len(interSection)*2) for _, v := range src { - if !ContainsFunc[T](interSection, v, equal) { + if !ContainsFunc[T](interSection, func(src T) bool { + return equal(src, v) + }) { ret = append(ret, v) } } for _, v := range dst { - if !ContainsFunc[T](interSection, v, equal) { + if !ContainsFunc[T](interSection, func(src T) bool { + return equal(src, v) + }) { ret = append(ret, v) } } diff --git a/slice/types.go b/slice/types.go index 79014b45..9e32fff1 100644 --- a/slice/types.go +++ b/slice/types.go @@ -16,3 +16,5 @@ package slice // equalFunc 比较两个元素是否相等 type equalFunc[T any] func(src, dst T) bool + +type matchFunc[T any] func(src T) bool From 6db61e48de1a2c6a66d9ffe6ee8f2dfc8d233c6e Mon Sep 17 00:00:00 2001 From: JohnWong <41528085+johnwongx@users.noreply.github.com> Date: Mon, 4 Sep 2023 23:11:33 +0800 Subject: [PATCH 24/32] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=B9=E7=A7=B0?= =?UTF-8?q?=E5=B7=AE=E9=9B=86=EF=BC=8C=E4=BA=A4=E9=9B=86=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=20(#208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 优化对称差集,交集实现 * 修改changelog --- .CHANGELOG.md | 1 + slice/add_test.go | 2 +- slice/intersect.go | 11 ++- slice/symmetric_diff.go | 53 +++++--------- slice/symmetric_diff_test.go | 138 +++++++++++++++++++++++++++-------- 5 files changed, 134 insertions(+), 71 deletions(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 7e88479d..5072d16a 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -18,6 +18,7 @@ - [slice: 添加Add函数,在指定位置插入元素](https://github.com/ecodeclub/ekit/pull/201) - [slice: 优化delete方法,无需从头开始遍历](https://github.com/ecodeclub/ekit/pull/203) - [slice: 重构 slice 中使用 equalFunc 的方法](https://github.com/ecodeclub/ekit/pull/205) +- [slice: intersect方法优化, symmetricDiffSet重构](https://github.com/ecodeclub/ekit/pull/208) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/slice/add_test.go b/slice/add_test.go index 014a9df3..078d2b35 100644 --- a/slice/add_test.go +++ b/slice/add_test.go @@ -24,7 +24,7 @@ import ( ) func TestAdd(t *testing.T) { - // Delete 主要依赖于 internal/slice.Delete 来保证正确性 + // Add 主要依赖于 internal/slice.Add 来保证正确性 testCases := []struct { name string slice []int diff --git a/slice/intersect.go b/slice/intersect.go index f82edeff..6ba45050 100644 --- a/slice/intersect.go +++ b/slice/intersect.go @@ -33,12 +33,11 @@ func IntersectSet[T comparable](src []T, dst []T) []T { // 已去重 func IntersectSetFunc[T any](src []T, dst []T, equal equalFunc[T]) []T { var ret = make([]T, 0, len(src)) - for _, valSrc := range src { - for _, valDst := range dst { - if equal(valDst, valSrc) { - ret = append(ret, valSrc) - break - } + for _, v := range dst { + if ContainsFunc[T](src, func(t T) bool { + return equal(t, v) + }) { + ret = append(ret, v) } } return deduplicateFunc[T](ret, equal) diff --git a/slice/symmetric_diff.go b/slice/symmetric_diff.go index d3bd1e9b..f31dab87 100644 --- a/slice/symmetric_diff.go +++ b/slice/symmetric_diff.go @@ -19,60 +19,45 @@ package slice // 返回值的元素顺序是不定的 func SymmetricDiffSet[T comparable](src, dst []T) []T { srcMap, dstMap := toMap[T](src), toMap[T](dst) - for dstKey := range dstMap { - if _, exist := srcMap[dstKey]; exist { - // 删除共同元素,两者剩余的并集即为对称差 - delete(dstMap, dstKey) - delete(srcMap, dstKey) + for k := range dstMap { + if _, ok := srcMap[k]; ok { + delete(srcMap, k) + } else { + srcMap[k] = struct{}{} } } - for k, v := range dstMap { - srcMap[k] = v - } - var ret = make([]T, 0, len(srcMap)) + res := make([]T, 0, len(srcMap)) for k := range srcMap { - ret = append(ret, k) + res = append(res, k) } - return ret + return res } // SymmetricDiffSetFunc 对称差集 // 你应该优先使用 SymmetricDiffSet // 已去重 func SymmetricDiffSetFunc[T any](src, dst []T, equal equalFunc[T]) []T { - var interSection = make([]T, 0, min(len(src), len(dst))) - for _, valSrc := range src { - for _, valDst := range dst { - if equal(valSrc, valDst) { - interSection = append(interSection, valSrc) - break - } - } - } + res := []T{} - ret := make([]T, 0, len(src)+len(dst)-len(interSection)*2) + //找出在src不在dst的元素 for _, v := range src { - if !ContainsFunc[T](interSection, func(src T) bool { - return equal(src, v) + if !ContainsFunc[T](dst, func(t T) bool { + return equal(t, v) }) { - ret = append(ret, v) + res = append(res, v) } } + + //找出在dst不在src的元素 for _, v := range dst { - if !ContainsFunc[T](interSection, func(src T) bool { - return equal(src, v) + if !ContainsFunc[T](src, func(t T) bool { + return equal(t, v) }) { - ret = append(ret, v) + res = append(res, v) } } - return deduplicateFunc[T](ret, equal) -} -func min(src, dst int) int { - if src > dst { - return dst - } - return src + return deduplicateFunc[T](res, equal) } diff --git a/slice/symmetric_diff_test.go b/slice/symmetric_diff_test.go index 7ba0f9da..59060c7b 100644 --- a/slice/symmetric_diff_test.go +++ b/slice/symmetric_diff_test.go @@ -30,31 +30,70 @@ func TestSymmetricDiffSet(t *testing.T) { want []int }{ { - src: []int{1, 2, 4, 3}, - dst: []int{4, 5, 6, 1}, - want: []int{2, 3, 5, 6}, - name: "normal test", + name: "no inter", + src: []int{1, 2, 3}, + dst: []int{4, 5, 6}, + want: []int{1, 2, 3, 4, 5, 6}, }, { - src: []int{1, 1, 2, 3, 4}, - dst: []int{4, 5, 6, 1, 7, 6}, - want: []int{3, 6, 7, 5, 2}, - name: "deduplicate", + name: "part inter", + src: []int{1, 2, 3}, + dst: []int{3, 4, 5}, + want: []int{1, 2, 4, 5}, }, { - src: []int{}, - dst: []int{1}, + name: "src contain dst", + src: []int{1, 2, 3}, + dst: []int{2, 3}, want: []int{1}, - name: "src length is 0", }, { - src: []int{1, 3, 5}, - dst: []int{2, 4}, - want: []int{1, 3, 2, 4, 5}, - name: "not exist same ele", + name: "dst contain src", + src: []int{4}, + dst: []int{4, 5, 6}, + want: []int{5, 6}, + }, + { + name: "equal", + src: []int{1, 2, 3}, + dst: []int{1, 2, 3}, + want: []int{}, + }, + { + name: "dst empty", + src: []int{1, 2, 3}, + dst: []int{}, + want: []int{1, 2, 3}, + }, + { + name: "src empty", + src: []int{}, + dst: []int{4, 5, 6}, + want: []int{4, 5, 6}, + }, + { + name: "all empty", + src: []int{}, + dst: []int{}, + want: []int{}, + }, + { + name: "src nil", + src: nil, + dst: []int{4, 5, 6}, + want: []int{4, 5, 6}, + }, + { + name: "dst nil", + src: []int{4, 5, 6}, + dst: nil, + want: []int{4, 5, 6}, }, { name: "both nil", + src: nil, + dst: nil, + want: []int{}, }, } for _, tt := range tests { @@ -73,31 +112,70 @@ func TestSymmetricDiffSetFunc(t *testing.T) { want []int }{ { - src: []int{1, 2, 3, 4}, - dst: []int{4, 5, 6, 1}, - want: []int{2, 3, 5, 6}, - name: "normal test", + name: "no inter", + src: []int{1, 2, 3}, + dst: []int{4, 5, 6}, + want: []int{1, 2, 3, 4, 5, 6}, }, { - src: []int{1, 1, 2, 3, 4}, - dst: []int{4, 5, 6, 1, 7, 6}, - want: []int{3, 6, 7, 5, 2}, - name: "deduplicate", + name: "part inter", + src: []int{1, 2, 3}, + dst: []int{3, 4, 5}, + want: []int{1, 2, 4, 5}, }, { - src: []int{}, - dst: []int{1}, + name: "src contain dst", + src: []int{1, 2, 3}, + dst: []int{2, 3}, want: []int{1}, - name: "src length is 0", }, { - src: []int{1, 3, 5}, - dst: []int{2, 4}, - want: []int{1, 3, 2, 4, 5}, - name: "not exist same ele", + name: "dst contain src", + src: []int{4}, + dst: []int{4, 5, 6}, + want: []int{5, 6}, + }, + { + name: "equal", + src: []int{1, 2, 3}, + dst: []int{1, 2, 3}, + want: []int{}, + }, + { + name: "dst empty", + src: []int{1, 2, 3}, + dst: []int{}, + want: []int{1, 2, 3}, + }, + { + name: "src empty", + src: []int{}, + dst: []int{4, 5, 6}, + want: []int{4, 5, 6}, + }, + { + name: "all empty", + src: []int{}, + dst: []int{}, + want: []int{}, + }, + { + name: "src nil", + src: nil, + dst: []int{4, 5, 6}, + want: []int{4, 5, 6}, + }, + { + name: "dst nil", + src: []int{4, 5, 6}, + dst: nil, + want: []int{4, 5, 6}, }, { name: "both nil", + src: nil, + dst: nil, + want: []int{}, }, } for _, tt := range tests { From e76aae0649941c6f06a469f982f95ef556d9427c Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Mon, 4 Sep 2023 23:34:03 +0800 Subject: [PATCH 25/32] =?UTF-8?q?sqlx:=20=E9=A2=84=E5=AE=9A=E4=B9=89=20Row?= =?UTF-8?q?s=20=E6=8E=A5=E5=8F=A3=20(#209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .CHANGELOG.md | 1 + sqlx/scanner.go | 5 ++--- sqlx/types.go | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 sqlx/types.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 5072d16a..c5018f17 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -8,6 +8,7 @@ - [sqlx:ScanRows 和 ScanAll方法](https://github.com/ecodeclub/ekit/pull/180) - [mapx: 修复红黑树删除节点问题](https://github.com/ecodeclub/ekit/pull/183) - [sqlx: 构建Scanner抽象替代现有ScanRows及ScanAll](https://github.com/ecodeclub/ekit/pull/182) +- [sqlx: 预定义 Rows 接口](https://github.com/ecodeclub/ekit/pull/209) - [pool: 重构TaskPool](https://github.com/ecodeclub/ekit/pull/184) - [syncx:Map 支持 LoadOrStoreFunc 方法](https://github.com/ecodeclub/ekit/pull/194) - [mapx: MutipleTreeMap](https://github.com/ecodeclub/ekit/pull/187) diff --git a/sqlx/scanner.go b/sqlx/scanner.go index d0c1d21d..8a3b223e 100644 --- a/sqlx/scanner.go +++ b/sqlx/scanner.go @@ -15,7 +15,6 @@ package sqlx import ( - "database/sql" "errors" "fmt" "reflect" @@ -35,12 +34,12 @@ type Scanner interface { } type sqlRowsScanner struct { - sqlRows *sql.Rows + sqlRows Rows columnValuePointers []any } // NewSQLRowsScanner 返回一个Scanner -func NewSQLRowsScanner(r *sql.Rows) (Scanner, error) { +func NewSQLRowsScanner(r Rows) (Scanner, error) { if r == nil { return nil, fmt.Errorf("%w *sql.Rows不能为nil", errInvalidArgument) } diff --git a/sqlx/types.go b/sqlx/types.go new file mode 100644 index 00000000..c68eb556 --- /dev/null +++ b/sqlx/types.go @@ -0,0 +1,36 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlx + +import "database/sql" + +// 因为 sql 包里面缺乏顶级接口定义,而在研发一些中间件的时候,又必须用到不同的实现 +// 因此在这里提前定义一些顶级接口 +// 一般来说,如果你不是设计一些和数据库有关的中间件,你是用不上这些接口的 + +var _ Rows = (*sql.Rows)(nil) + +type Rows interface { + Next() bool + NextResultSet() bool + Err() error + Columns() ([]string, error) + // ColumnTypes 还是返回了原本的 sql.ColumnType + // 因为 ColumnType 同样不是一个接口,所以为了兼容 sql.Rows, + // 就只有保持这个设计 + ColumnTypes() ([]*sql.ColumnType, error) + Scan(dest ...any) error + Close() error +} From abbebbd58aa010571724c26677a5f623ab6175ce Mon Sep 17 00:00:00 2001 From: Zhang Cancan <102995829+cancan927@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:58:53 +0800 Subject: [PATCH 26/32] =?UTF-8?q?randx=EF=BC=9A=E6=96=B0=E5=A2=9E=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=9A=8F=E6=9C=BAcode=E6=96=B9=E6=B3=95=20(#207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 优化slice的delete方法的性能 * 添加到ChangeLog中 * 新增生成随机code方法 * 修改changelog * randx:修改代码提升可读性提升 * randx:增加测试覆盖 * randx:测试修改为table driven方式 * randx:测试修改 --------- Signed-off-by: Zhang Cancan <102995829+cancan927@users.noreply.github.com> --- .CHANGELOG.md | 2 + randx/rand_code.go | 88 +++++++++++++++++++++++++++++++++++++++ randx/rand_code_test.go | 92 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 randx/rand_code.go create mode 100644 randx/rand_code_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index c5018f17..a74fee07 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -19,8 +19,10 @@ - [slice: 添加Add函数,在指定位置插入元素](https://github.com/ecodeclub/ekit/pull/201) - [slice: 优化delete方法,无需从头开始遍历](https://github.com/ecodeclub/ekit/pull/203) - [slice: 重构 slice 中使用 equalFunc 的方法](https://github.com/ecodeclub/ekit/pull/205) +- [randx: 新增生成随机code方法](https://github.com/ecodeclub/ekit/pull/207) - [slice: intersect方法优化, symmetricDiffSet重构](https://github.com/ecodeclub/ekit/pull/208) + # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) - [reflectx: IsNil 方法](https://github.com/ecodeclub/ekit/pull/150) diff --git a/randx/rand_code.go b/randx/rand_code.go new file mode 100644 index 00000000..776d0559 --- /dev/null +++ b/randx/rand_code.go @@ -0,0 +1,88 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package randx + +import ( + "errors" + "math/rand" +) + +var ERRTYPENOTSUPPORTTED = errors.New("ekit:不支持的类型") + +type TYPE int + +const ( + TYPE_DEFAULT TYPE = 0 //默认类型 + TYPE_DIGIT TYPE = 1 //数字// + TYPE_LETTER TYPE = 2 //小写字母 + TYPE_CAPITAL TYPE = 3 //大写字母 + TYPE_MIXED TYPE = 4 //数字+字母混合 +) + +// RandCode 根据传入的长度和类型生成随机字符串,这个方法目前可以生成数字、字母、数字+字母的随机字符串 +func RandCode(length int, typ TYPE) (string, error) { + switch typ { + case TYPE_DEFAULT: + fallthrough + case TYPE_DIGIT: + return generate("0123456789", length, 4), nil + case TYPE_LETTER: + return generate("abcdefghijklmnopqrstuvwxyz", length, 5), nil + case TYPE_CAPITAL: + return generate("ABCDEFGHIJKLMNOPQRSTUVWXYZ", length, 5), nil + case TYPE_MIXED: + return generate("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", length, 7), nil + default: + return "", ERRTYPENOTSUPPORTTED + } +} + +// generate 根据传入的随机源和长度生成随机字符串,一次随机,多次使用 +func generate(source string, length, idxBits int) string { + + //掩码 + //例如: 使用低6位:0000 0000 --> 0011 1111 + idxMask := 1<>= idxBits + + //扣减remain + remain-- + + } + return string(result) + +} diff --git a/randx/rand_code_test.go b/randx/rand_code_test.go new file mode 100644 index 00000000..ee962eee --- /dev/null +++ b/randx/rand_code_test.go @@ -0,0 +1,92 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package randx + +import ( + "errors" + "regexp" + "testing" +) + +func TestRandCode(t *testing.T) { + testCases := []struct { + name string + length int + typ TYPE + wantMatch string + wantErr error + }{ + { + name: "默认类型", + length: 8, + typ: TYPE_DEFAULT, + wantMatch: "^[0-9]+$", + wantErr: nil, + }, + { + name: "数字验证码", + length: 8, + typ: TYPE_DIGIT, + wantMatch: "^[0-9]+$", + wantErr: nil, + }, { + name: "小写字母验证码", + length: 8, + typ: TYPE_LETTER, + wantMatch: "^[a-z]+$", + wantErr: nil, + }, { + name: "大写字母验证码", + length: 8, + typ: TYPE_CAPITAL, + wantMatch: "^[A-Z]+$", + wantErr: nil, + }, { + name: "混合验证码", + length: 8, + typ: TYPE_MIXED, + wantMatch: "^[0-9a-zA-Z]+$", + wantErr: nil, + }, { + name: "未定义类型", + length: 8, + typ: 9, + wantMatch: "", + wantErr: ERRTYPENOTSUPPORTTED, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + code, err := RandCode(tc.length, tc.typ) + if err != nil { + if !errors.Is(err, tc.wantErr) { + t.Errorf("unexpected error: %v", err) + } + } else { + //长度检验 + if len(code) != tc.length { + t.Errorf("expected length: %d but got length:%d ", tc.length, len(code)) + } + //模式检验 + matched, _ := regexp.MatchString(tc.wantMatch, code) + if !matched { + t.Errorf("expected %s but got %s", tc.wantMatch, code) + } + } + }) + } + +} From 6a977f2410044399941f377dde2cebf700d88d16 Mon Sep 17 00:00:00 2001 From: JohnWong <41528085+johnwongx@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:22:31 +0800 Subject: [PATCH 27/32] =?UTF-8?q?=E4=BF=AE=E5=A4=8DEncryptColumn.Scan=20st?= =?UTF-8?q?ring=E5=88=86=E6=94=AF=E9=94=99=E8=AF=AF=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复EncryptColumn.Scan string分支错误 * 修改changelog * 修改changelog信息错误 --- .CHANGELOG.md | 1 + sqlx/encrypt.go | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index a74fee07..0b7444bb 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -21,6 +21,7 @@ - [slice: 重构 slice 中使用 equalFunc 的方法](https://github.com/ecodeclub/ekit/pull/205) - [randx: 新增生成随机code方法](https://github.com/ecodeclub/ekit/pull/207) - [slice: intersect方法优化, symmetricDiffSet重构](https://github.com/ecodeclub/ekit/pull/208) +- [sqlx: 修复EncryptColumn Scan方法string分支错误](https://github.com/ecodeclub/ekit/pull/211) # v0.0.7 diff --git a/sqlx/encrypt.go b/sqlx/encrypt.go index a932179b..0fdee3c0 100644 --- a/sqlx/encrypt.go +++ b/sqlx/encrypt.go @@ -93,9 +93,6 @@ func (e *EncryptColumn[T]) Scan(src any) error { b, err = e.aesDecrypt(value) case string: b, err = e.aesDecrypt([]byte(value)) - if err != nil { - return nil - } default: return fmt.Errorf("ekit:EncryptColumn.Scan 不支持 src 类型 %v", src) } From cb15834bcec93acd507b82eefde808c3169d2733 Mon Sep 17 00:00:00 2001 From: Ming Deng Date: Mon, 11 Sep 2023 23:39:07 +0800 Subject: [PATCH 28/32] =?UTF-8?q?sqlx:=20Scanner=20=E6=B7=BB=E5=8A=A0=20Ne?= =?UTF-8?q?xtResultSet=20=E6=96=B9=E6=B3=95=20(#212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .CHANGELOG.md | 1 + sqlx/scanner.go | 7 +++++++ sqlx/scanner_test.go | 33 +++++++++++++++++++++++++++++++++ value.go | 15 +++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 0b7444bb..38e3ba6a 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -22,6 +22,7 @@ - [randx: 新增生成随机code方法](https://github.com/ecodeclub/ekit/pull/207) - [slice: intersect方法优化, symmetricDiffSet重构](https://github.com/ecodeclub/ekit/pull/208) - [sqlx: 修复EncryptColumn Scan方法string分支错误](https://github.com/ecodeclub/ekit/pull/211) +- [sqlx: Scanner 添加 NextResultSet 方法](https://github.com/ecodeclub/ekit/pull/212) # v0.0.7 diff --git a/sqlx/scanner.go b/sqlx/scanner.go index 8a3b223e..5b1b296e 100644 --- a/sqlx/scanner.go +++ b/sqlx/scanner.go @@ -30,7 +30,10 @@ var ( // Scanner 不会关闭sql.Rows,用户需要对此负责 type Scanner interface { Scan() (values []any, err error) + // ScanAll 扫描当前结果集的全部数据 ScanAll() (allValues [][]any, err error) + // NextResultSet 移动到下一个结果集 + NextResultSet() bool } type sqlRowsScanner struct { @@ -58,6 +61,10 @@ func NewSQLRowsScanner(r Rows) (Scanner, error) { return &sqlRowsScanner{sqlRows: r, columnValuePointers: columnValuePointers}, nil } +func (s *sqlRowsScanner) NextResultSet() bool { + return s.sqlRows.NextResultSet() +} + // Scan 返回一行 func (s *sqlRowsScanner) Scan() ([]any, error) { if !s.sqlRows.Next() { diff --git a/sqlx/scanner_test.go b/sqlx/scanner_test.go index be66eda2..77756c0d 100644 --- a/sqlx/scanner_test.go +++ b/sqlx/scanner_test.go @@ -261,3 +261,36 @@ func TestSqlRowsScanner_ScanAll(t *testing.T) { assert.Equal(t, expectedErr, err) }) } + +func TestSqlRowsScanner_NextResultSet(t *testing.T) { + t.Parallel() + t.Run("没有更多 ResultSet", func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + mock.ExpectQuery("SELECT .*"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + rows, err := db.Query("SELECT id, name FROM users") + require.NoError(t, err) + scanner, err := NewSQLRowsScanner(rows) + require.NoError(t, err) + assert.False(t, scanner.NextResultSet()) + }) + t.Run("还有 ResultSet", func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + mock.ExpectQuery("SELECT .*"). + WillReturnRows( + sqlmock.NewRows([]string{"id", "name"}), + sqlmock.NewRows([]string{"id", "name"}), + sqlmock.NewRows([]string{"id", "name"})) + rows, err := db.Query("SELECT id, name FROM users") + require.NoError(t, err) + scanner, err := NewSQLRowsScanner(rows) + require.NoError(t, err) + assert.True(t, scanner.NextResultSet()) + assert.True(t, scanner.NextResultSet()) + assert.False(t, scanner.NextResultSet()) + }) +} diff --git a/value.go b/value.go index 96beed0e..ed78978f 100644 --- a/value.go +++ b/value.go @@ -16,6 +16,7 @@ package ekit import ( "reflect" + "strconv" "github.com/ecodeclub/ekit/internal/errs" ) @@ -38,6 +39,20 @@ func (av AnyValue) Int() (int, error) { return val, nil } +func (av AnyValue) AsInt() (int, error) { + if av.Err != nil { + return 0, av.Err + } + switch v := av.Val.(type) { + case int: + return v, nil + case string: + res, err := strconv.ParseInt(v, 10, 64) + return int(res), err + } + return 0, errs.NewErrInvalidType("int", reflect.TypeOf(av.Val).String()) +} + // IntOrDefault 返回 int 数据,或者默认值 func (av AnyValue) IntOrDefault(def int) int { val, err := av.Int() From b55e7b25757d71335439cb458adad2f4b0d82cdf Mon Sep 17 00:00:00 2001 From: Depravity-pig Date: Thu, 14 Sep 2023 21:53:27 +0800 Subject: [PATCH 29/32] =?UTF-8?q?[feat]:=20(=E6=96=B0=E5=A2=9Estring?= =?UTF-8?q?=E7=89=B9=E6=80=A7)=20AnyValue=20=E6=94=AF=E6=8C=81=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E8=BD=AC=E6=8D=A2=20-=20String=20=E8=BD=AC=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=20#210=20=20(#213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat]: (新增string特性) AnyValue 支持类型转换 - String 转类型 #210 实现 string 类型转换 As[Type] * int 家族:int, int8, int16. int32, int64 * uint 家族: uint, uint8, uint16, uint64 * float 家族: float32, float64 * []byte * [test] 新增As方法测试用例 * 新增changelog记录 --- .CHANGELOG.md | 1 + .gitignore | 2 +- value.go | 277 ++++++++++++++++ value_test.go | 859 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1138 insertions(+), 1 deletion(-) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 38e3ba6a..7af458f8 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -23,6 +23,7 @@ - [slice: intersect方法优化, symmetricDiffSet重构](https://github.com/ecodeclub/ekit/pull/208) - [sqlx: 修复EncryptColumn Scan方法string分支错误](https://github.com/ecodeclub/ekit/pull/211) - [sqlx: Scanner 添加 NextResultSet 方法](https://github.com/ecodeclub/ekit/pull/212) +- [ekit: AnyValue 支持As[Type]类型 String 转换](https://github.com/ecodeclub/ekit/pull/213) # v0.0.7 diff --git a/.gitignore b/.gitignore index 13da0af5..b12cde5d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,5 @@ # vendor/ .idea - +.vscode **/.DS_Store \ No newline at end of file diff --git a/value.go b/value.go index ed78978f..a2e89bb4 100644 --- a/value.go +++ b/value.go @@ -15,6 +15,8 @@ package ekit import ( + "errors" + "fmt" "reflect" "strconv" @@ -74,6 +76,20 @@ func (av AnyValue) Uint() (uint, error) { return val, nil } +func (av AnyValue) AsUint() (uint, error) { + if av.Err != nil { + return 0, av.Err + } + switch v := av.Val.(type) { + case uint: + return v, nil + case string: + res, err := strconv.ParseUint(v, 10, 64) + return uint(res), err + } + return 0, errs.NewErrInvalidType("uint", reflect.TypeOf(av.Val).String()) +} + // UintOrDefault 返回 uint 数据,或者默认值 func (av AnyValue) UintOrDefault(def uint) uint { val, err := av.Uint() @@ -83,6 +99,142 @@ func (av AnyValue) UintOrDefault(def uint) uint { return val } +func (av AnyValue) Int8() (int8, error) { + if av.Err != nil { + return 0, av.Err + } + val, ok := av.Val.(int8) + if !ok { + return 0, errs.NewErrInvalidType("int", reflect.TypeOf(av.Val).String()) + } + return val, nil +} + +func (av AnyValue) AsInt8() (int8, error) { + if av.Err != nil { + return 0, av.Err + } + + switch v := av.Val.(type) { + case int8: + return v, nil + case string: + res, err := strconv.ParseInt(v, 10, 64) + return int8(res), err + } + return 0, errs.NewErrInvalidType("int8", reflect.TypeOf(av.Val).String()) +} + +func (av AnyValue) Int8OrDefault(def int8) int8 { + val, err := av.Int8() + if err != nil { + return def + } + return val +} + +func (av AnyValue) Uint8() (uint8, error) { + if av.Err != nil { + return 0, av.Err + } + val, ok := av.Val.(uint8) + if !ok { + return 0, errs.NewErrInvalidType("uint8", reflect.TypeOf(av.Val).String()) + } + return val, nil +} + +func (av AnyValue) AsUint8() (uint8, error) { + if av.Err != nil { + return 0, av.Err + } + + switch v := av.Val.(type) { + case uint8: + return v, nil + case string: + res, err := strconv.ParseUint(v, 10, 8) + return uint8(res), err + } + return 0, errs.NewErrInvalidType("uint8", reflect.TypeOf(av.Val).String()) +} + +func (av AnyValue) Uint8OrDefault(def uint8) uint8 { + val, err := av.Uint8() + if err != nil { + return def + } + return val +} + +func (av AnyValue) Int16() (int16, error) { + if av.Err != nil { + return 0, av.Err + } + val, ok := av.Val.(int16) + if !ok { + return 0, errs.NewErrInvalidType("int16", reflect.TypeOf(av.Val).String()) + } + return val, nil +} + +func (av AnyValue) AsInt16() (int16, error) { + if av.Err != nil { + return 0, av.Err + } + + switch v := av.Val.(type) { + case int16: + return v, nil + case string: + res, err := strconv.ParseInt(v, 10, 16) + return int16(res), err + } + return 0, errs.NewErrInvalidType("int16", reflect.TypeOf(av.Val).String()) +} + +func (av AnyValue) Int16OrDefault(def int16) int16 { + val, err := av.Int16() + if err != nil { + return def + } + return val +} + +func (av AnyValue) Uint16() (uint16, error) { + if av.Err != nil { + return 0, av.Err + } + val, ok := av.Val.(uint16) + if !ok { + return 0, errs.NewErrInvalidType("uint16", reflect.TypeOf(av.Val).String()) + } + return val, nil +} + +func (av AnyValue) AsUint16() (uint16, error) { + if av.Err != nil { + return 0, av.Err + } + + switch v := av.Val.(type) { + case uint16: + return v, nil + case string: + res, err := strconv.ParseUint(v, 10, 16) + return uint16(res), err + } + return 0, errs.NewErrInvalidType("uint16", reflect.TypeOf(av.Val).String()) +} + +func (av AnyValue) Uint16OrDefault(def uint16) uint16 { + val, err := av.Uint16() + if err != nil { + return def + } + return val +} + // Int32 返回 int32 数据 func (av AnyValue) Int32() (int32, error) { if av.Err != nil { @@ -95,6 +247,20 @@ func (av AnyValue) Int32() (int32, error) { return val, nil } +func (av AnyValue) AsInt32() (int32, error) { + if av.Err != nil { + return 0, av.Err + } + switch v := av.Val.(type) { + case int32: + return v, nil + case string: + res, err := strconv.ParseInt(v, 10, 32) + return int32(res), err + } + return 0, errs.NewErrInvalidType("int32", reflect.TypeOf(av.Val).String()) +} + // Int32OrDefault 返回 int32 数据,或者默认值 func (av AnyValue) Int32OrDefault(def int32) int32 { val, err := av.Int32() @@ -116,6 +282,20 @@ func (av AnyValue) Uint32() (uint32, error) { return val, nil } +func (av AnyValue) AsUint32() (uint32, error) { + if av.Err != nil { + return 0, av.Err + } + switch v := av.Val.(type) { + case uint32: + return v, nil + case string: + res, err := strconv.ParseUint(v, 10, 32) + return uint32(res), err + } + return 0, errs.NewErrInvalidType("uint32", reflect.TypeOf(av.Val).String()) +} + // Uint32OrDefault 返回 uint32 数据,或者默认值 func (av AnyValue) Uint32OrDefault(def uint32) uint32 { val, err := av.Uint32() @@ -137,6 +317,19 @@ func (av AnyValue) Int64() (int64, error) { return val, nil } +func (av AnyValue) AsInt64() (int64, error) { + if av.Err != nil { + return 0, av.Err + } + switch v := av.Val.(type) { + case int64: + return v, nil + case string: + return strconv.ParseInt(v, 10, 64) + } + return 0, errs.NewErrInvalidType("int64", reflect.TypeOf(av.Val).String()) +} + // Int64OrDefault 返回 int64 数据,或者默认值 func (av AnyValue) Int64OrDefault(def int64) int64 { val, err := av.Int64() @@ -158,6 +351,19 @@ func (av AnyValue) Uint64() (uint64, error) { return val, nil } +func (av AnyValue) AsUint64() (uint64, error) { + if av.Err != nil { + return 0, av.Err + } + switch v := av.Val.(type) { + case uint64: + return v, nil + case string: + return strconv.ParseUint(v, 10, 64) + } + return 0, errs.NewErrInvalidType("uint64", reflect.TypeOf(av.Val).String()) +} + // Uint64OrDefault 返回 uint64 数据,或者默认值 func (av AnyValue) Uint64OrDefault(def uint64) uint64 { val, err := av.Uint64() @@ -179,6 +385,20 @@ func (av AnyValue) Float32() (float32, error) { return val, nil } +func (av AnyValue) AsFloat32() (float32, error) { + if av.Err != nil { + return 0, av.Err + } + switch v := av.Val.(type) { + case float32: + return v, nil + case string: + res, err := strconv.ParseFloat(v, 32) + return float32(res), err + } + return 0, errs.NewErrInvalidType("float32", reflect.TypeOf(av.Val).String()) +} + // Float32OrDefault 返回 float32 数据,或者默认值 func (av AnyValue) Float32OrDefault(def float32) float32 { val, err := av.Float32() @@ -200,6 +420,19 @@ func (av AnyValue) Float64() (float64, error) { return val, nil } +func (av AnyValue) AsFloat64() (float64, error) { + if av.Err != nil { + return 0, av.Err + } + switch v := av.Val.(type) { + case float64: + return v, nil + case string: + return strconv.ParseFloat(v, 64) + } + return 0, errs.NewErrInvalidType("float64", reflect.TypeOf(av.Val).String()) +} + // Float64OrDefault 返回 float64 数据,或者默认值 func (av AnyValue) Float64OrDefault(def float64) float64 { val, err := av.Float64() @@ -221,6 +454,36 @@ func (av AnyValue) String() (string, error) { return val, nil } +func (av AnyValue) AsString() (string, error) { + if av.Err != nil { + return "", av.Err + } + + var val string + valueOf := reflect.ValueOf(av.Val) + switch valueOf.Type().Kind() { + case reflect.String: + val = valueOf.String() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + val = strconv.FormatUint(valueOf.Uint(), 10) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + val = strconv.FormatInt(valueOf.Int(), 10) + case reflect.Float32: + val = strconv.FormatFloat(valueOf.Float(), 'f', 10, 32) + case reflect.Float64: + val = strconv.FormatFloat(valueOf.Float(), 'f', 10, 64) + case reflect.Slice: + if valueOf.Type().Elem().Kind() != reflect.Uint8 { + return "", errs.NewErrInvalidType("[]byte", fmt.Sprintf("[]%s", valueOf.Type().Elem().Kind())) + } + val = string(valueOf.Bytes()) + default: + return "", errors.New("未兼容类型,暂时无法转换") + } + + return val, nil +} + // StringOrDefault 返回 string 数据,或者默认值 func (av AnyValue) StringOrDefault(def string) string { val, err := av.String() @@ -242,6 +505,20 @@ func (av AnyValue) Bytes() ([]byte, error) { return val, nil } +func (av AnyValue) AsBytes() ([]byte, error) { + if av.Err != nil { + return []byte{}, av.Err + } + switch v := av.Val.(type) { + case []byte: + return v, nil + case string: + return []byte(v), nil + } + + return []byte{}, errs.NewErrInvalidType("[]byte", reflect.TypeOf(av.Val).String()) +} + // BytesOrDefault 返回 []byte 数据,或者默认值 func (av AnyValue) BytesOrDefault(def []byte) []byte { val, err := av.Bytes() diff --git a/value_test.go b/value_test.go index a0cee2cb..8e9b9002 100644 --- a/value_test.go +++ b/value_test.go @@ -970,3 +970,862 @@ func TestAnyValue_BoolOrDefault(t *testing.T) { }) } } + +func TestAnyValue_Int8OrDefault(t *testing.T) { + tests := []struct { + name string + val AnyValue + def int8 + want int8 + }{ + { + name: "normal case:", + val: AnyValue{ + Val: int8(1), + }, + want: 1, + }, + { + name: "default case:", + val: AnyValue{ + Val: int8(0), + Err: errors.New("error"), + }, + def: 1, + want: 1, + }, + { + name: "type error case:", + val: AnyValue{ + Val: true, + }, + def: 10, + want: 10, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + av := AnyValue{ + Val: tt.val.Val, + Err: tt.val.Err, + } + assert.Equal(t, av.Int8OrDefault(tt.def), tt.want) + }) + } +} + +func TestAnyValue_Int16OrDefault(t *testing.T) { + tests := []struct { + name string + val AnyValue + def int16 + want int16 + }{ + { + name: "normal case:", + val: AnyValue{ + Val: int16(1), + }, + want: 1, + }, + { + name: "default case:", + val: AnyValue{ + Val: int16(0), + Err: errors.New("error"), + }, + def: 1, + want: 1, + }, + { + name: "type error case:", + val: AnyValue{ + Val: true, + }, + def: 10, + want: 10, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + av := AnyValue{ + Val: tt.val.Val, + Err: tt.val.Err, + } + assert.Equal(t, av.Int16OrDefault(tt.def), tt.want) + }) + } +} + +func TestAnyValue_Uint8OrDefault(t *testing.T) { + tests := []struct { + name string + val AnyValue + def uint8 + want uint8 + }{ + { + name: "normal case:", + val: AnyValue{ + Val: uint8(1), + }, + want: 1, + }, + { + name: "default case:", + val: AnyValue{ + Val: uint8(0), + Err: errors.New("error"), + }, + def: 1, + want: 1, + }, + { + name: "type error case:", + val: AnyValue{ + Val: true, + }, + def: 10, + want: 10, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + av := AnyValue{ + Val: tt.val.Val, + Err: tt.val.Err, + } + assert.Equal(t, av.Uint8OrDefault(tt.def), tt.want) + }) + } +} + +func TestAnyValue_Uint16OrDefault(t *testing.T) { + tests := []struct { + name string + val AnyValue + def uint16 + want uint16 + }{ + { + name: "normal case:", + val: AnyValue{ + Val: uint16(1), + }, + want: 1, + }, + { + name: "default case:", + val: AnyValue{ + Val: uint16(0), + Err: errors.New("error"), + }, + def: 1, + want: 1, + }, + { + name: "type error case:", + val: AnyValue{ + Val: true, + }, + def: 10, + want: 10, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + av := AnyValue{ + Val: tt.val.Val, + Err: tt.val.Err, + } + assert.Equal(t, av.Uint16OrDefault(tt.def), tt.want) + }) + } +} + +func TestAnyValue_AsInt(t *testing.T) { + tests := []struct { + name string + val AnyValue + want int + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal int case:", + val: AnyValue{ + Val: int(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("int", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsInt() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsInt8(t *testing.T) { + tests := []struct { + name string + val AnyValue + want int8 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal int case:", + val: AnyValue{ + Val: int8(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("int8", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsInt8() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsInt16(t *testing.T) { + tests := []struct { + name string + val AnyValue + want int16 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal int16 case:", + val: AnyValue{ + Val: int16(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("int16", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsInt16() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsInt32(t *testing.T) { + tests := []struct { + name string + val AnyValue + want int32 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal int32 case:", + val: AnyValue{ + Val: int32(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("int32", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsInt32() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsInt64(t *testing.T) { + tests := []struct { + name string + val AnyValue + want int64 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal int64 case:", + val: AnyValue{ + Val: int64(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("int64", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsInt64() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsUint(t *testing.T) { + tests := []struct { + name string + val AnyValue + want uint + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal uint case:", + val: AnyValue{ + Val: uint(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("uint", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsUint() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsUint8(t *testing.T) { + tests := []struct { + name string + val AnyValue + want uint8 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal uint8 case:", + val: AnyValue{ + Val: uint8(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("uint8", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsUint8() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsUint16(t *testing.T) { + tests := []struct { + name string + val AnyValue + want uint16 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal uint16 case:", + val: AnyValue{ + Val: uint16(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("uint16", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsUint16() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsUint32(t *testing.T) { + tests := []struct { + name string + val AnyValue + want uint32 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal uint32 case:", + val: AnyValue{ + Val: uint32(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("uint32", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsUint32() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsUint64(t *testing.T) { + tests := []struct { + name string + val AnyValue + want uint64 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1", + }, + want: 1, + }, + { + name: "normal uint64 case:", + val: AnyValue{ + Val: uint64(2), + }, + want: 2, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("uint64", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Val: "", + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsUint64() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsFloat32(t *testing.T) { + tests := []struct { + name string + val AnyValue + want float32 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "1.01", + }, + want: 1.01, + }, + { + name: "normal float32 case:", + val: AnyValue{ + Val: float32(2.44), + }, + want: 2.44, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("float32", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsFloat32() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsFloat64(t *testing.T) { + tests := []struct { + name string + val AnyValue + want float64 + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "100.0000000000", + }, + want: 1e2, + }, + { + name: "normal float64 case:", + val: AnyValue{ + Val: float64(2.44), + }, + want: 2.44, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + err: errs.NewErrInvalidType("float64", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Err: errors.New("error"), + }, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsFloat64() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsBytes(t *testing.T) { + tests := []struct { + name string + val AnyValue + want []byte + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "hello", + }, + want: []byte("hello"), + }, + { + name: "normal []byte case:", + val: AnyValue{ + Val: []byte{1}, + }, + want: []byte{1}, + }, + { + name: "type error case:", + val: AnyValue{ + Val: []int{1}, + }, + want: []byte{}, + err: errs.NewErrInvalidType("[]byte", "[]int"), + }, + { + name: "value exists error case:", + val: AnyValue{ + Err: errors.New("error"), + }, + want: []byte{}, + err: errors.New("error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsBytes() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestAnyValue_AsString(t *testing.T) { + tests := []struct { + name string + val AnyValue + want string + err error + }{ + { + name: "normal string case:", + val: AnyValue{ + Val: "hello ekit", + }, + want: "hello ekit", + }, + { + name: "normal uint case:", + val: AnyValue{ + Val: uint16(1231), + }, + want: "1231", + }, + { + name: "normal int case:", + val: AnyValue{ + Val: 1, + }, + want: "1", + }, + { + name: "normal float case:", + val: AnyValue{ + Val: 1e2, + }, + want: "100.0000000000", + }, + { + name: "normal []byte case:", + val: AnyValue{ + Val: []byte{72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}, + }, + want: "Hello, World!", + }, + { + name: "type conversion failed", + val: AnyValue{ + Val: []string{"h", "e", "llo"}, + }, + err: errs.NewErrInvalidType("[]byte", "[]string"), + }, + { + name: "type conversion failed by int", + val: AnyValue{ + Val: []int{1, 2, 3, 4, 5}, + }, + err: errs.NewErrInvalidType("[]byte", "[]int"), + }, + { + name: "unsupported type case:", + val: AnyValue{ + Val: map[string]any{ + "test": 1, + "hhh": "sss", + }, + }, + err: errors.New("未兼容类型,暂时无法转换"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val, err := tt.val.AsString() + assert.Equal(t, tt.want, val) + assert.Equal(t, tt.err, err) + }) + } +} From 7356bbf4ef59d1bc4852688487d8ec8890a14d4f Mon Sep 17 00:00:00 2001 From: Depravity-pig Date: Fri, 15 Sep 2023 23:40:53 +0800 Subject: [PATCH 30/32] =?UTF-8?q?[feat](stringx)=20=E6=96=B0=E5=A2=9Eunsaf?= =?UTF-8?q?e=20=E8=BD=AC=E6=8D=A2=20string=20=E5=92=8C=20[]byte=20(#215)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat](stringx) 新增unsafe 转换 string 和 []byte 注意使用非安全的方式转换时时一定要注意以下三点 - 确保传入的字符串和字节切片的生命周期足够长,不会在转换后被释放或修改。 - 确保传入的字符串和字节切片的长度和容量是一致的,否则可能导致访问越界。 - 不要对转换后的字节切片或字符串进行修改,因为它们可能与原始的字符串或字节切片共享底层的内存。 * 添加 changelog 记录 * 完善测试用例及注释 --- .CHANGELOG.md | 2 +- stringx/string.go | 37 ++++++++++++ stringx/string_test.go | 128 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 stringx/string.go create mode 100644 stringx/string_test.go diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 7af458f8..18bf6e14 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -24,7 +24,7 @@ - [sqlx: 修复EncryptColumn Scan方法string分支错误](https://github.com/ecodeclub/ekit/pull/211) - [sqlx: Scanner 添加 NextResultSet 方法](https://github.com/ecodeclub/ekit/pull/212) - [ekit: AnyValue 支持As[Type]类型 String 转换](https://github.com/ecodeclub/ekit/pull/213) - +- [stringx: unsafe 转换 string 和 []byte](https://github.com/ecodeclub/ekit/pull/215) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/stringx/string.go b/stringx/string.go new file mode 100644 index 00000000..91572221 --- /dev/null +++ b/stringx/string.go @@ -0,0 +1,37 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stringx + +import ( + "unsafe" +) + +// 确保传入的字符串和字节切片的生命周期足够长,不会在转换后被释放或修改。 +// 确保传入的字符串和字节切片的长度和容量是一致的,否则可能导致访问越界。 +// 不要对转换后的字节切片或字符串进行修改,因为它们可能与原始的字符串或字节切片共享底层的内存。 + +// UnsafeToBytes 非安全 string 转 []byte 他必须遵守上述规则 +func UnsafeToBytes(val string) []byte { + sh := (*[2]uintptr)(unsafe.Pointer(&val)) + bh := [3]uintptr{sh[0], sh[1], sh[1]} + return *(*[]byte)(unsafe.Pointer(&bh)) +} + +// UnsafeToString 非安全 []byte 转 string 他必须遵守上述规则 +func UnsafeToString(val []byte) string { + bh := (*[3]uintptr)(unsafe.Pointer(&val)) + sh := [2]uintptr{bh[0], bh[1]} + return *(*string)(unsafe.Pointer(&sh)) +} diff --git a/stringx/string_test.go b/stringx/string_test.go new file mode 100644 index 00000000..a2f6ab2e --- /dev/null +++ b/stringx/string_test.go @@ -0,0 +1,128 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stringx + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnsafeToBytes(t *testing.T) { + testCase := []struct { + name string + val string + want []byte + }{ + { + name: "normal conversion", + val: "hello", + want: []byte("hello"), + }, + { + name: "emoji coversion", + val: "😀!hello world", + want: []byte("😀!hello world"), + }, + { + name: "chinese coversion", + val: "你好 世界!", + want: []byte("你好 世界!"), + }, + } + + for _, tt := range testCase { + t.Run(tt.name, func(t *testing.T) { + val := UnsafeToBytes(tt.val) + assert.Equal(t, tt.want, val) + }) + } +} + +func TestUnsafeToString(t *testing.T) { + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + val func(t *testing.T) []byte + want string + }{ + { + name: "normal conversion", + before: func(t *testing.T) {}, + after: func(t *testing.T) {}, + val: func(t *testing.T) []byte { + return []byte("hello") + }, + want: "hello", + }, + { + name: "emoji coversion", + before: func(t *testing.T) {}, + after: func(t *testing.T) {}, + val: func(t *testing.T) []byte { + return []byte("😀!hello world") + }, + want: "😀!hello world", + }, + { + name: "chinese coversion", + before: func(t *testing.T) {}, + after: func(t *testing.T) {}, + val: func(t *testing.T) []byte { + return []byte("你好 世界!") + }, + want: "你好 世界!", + }, + { + // 通过读取 file 文件 模拟 io.Reader 中存在的字节流 并将其转换为 string 检查他的正确性 + // 当然他必须是可控制的 + name: "file(io.Reader) read bytes stream coversion string", + before: func(t *testing.T) { + create, err := os.Create("/tmp/test_put.txt") + require.NoError(t, err) + defer create.Close() + _, err = create.WriteString("the test file...") + require.NoError(t, err) + }, + after: func(t *testing.T) { + require.NoError(t, os.Remove("/tmp/test_put.txt")) + }, + val: func(t *testing.T) []byte { + open, err := os.Open("/tmp/test_put.txt") + require.NoError(t, err) + defer open.Close() + buf := bytes.Buffer{} + _, err = buf.ReadFrom(open) + require.NoError(t, err) + return buf.Bytes() + }, + want: "the test file...", + }, + } + + for _, tt := range testCase { + t.Run(tt.name, func(t *testing.T) { + defer tt.after(t) + tt.before(t) + b := tt.val(t) + val := UnsafeToString(b) + assert.Equal(t, tt.want, val) + }) + } +} From ee80839237b9276759a16e0dddaff9fcf21ee9b7 Mon Sep 17 00:00:00 2001 From: Depravity-pig Date: Sun, 17 Sep 2023 22:47:48 +0800 Subject: [PATCH 31/32] =?UTF-8?q?=E6=96=B0=E5=A2=9Estringx=20Benchmark=20(?= =?UTF-8?q?#216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 新增stringx Benchmark * 添加changelog --- .CHANGELOG.md | 1 + stringx/string_test.go | 32 ++++++++++++++++++++++++++++++++ stringx/stringx_benchmark | 10 ++++++++++ 3 files changed, 43 insertions(+) create mode 100644 stringx/stringx_benchmark diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 18bf6e14..5b28f312 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -25,6 +25,7 @@ - [sqlx: Scanner 添加 NextResultSet 方法](https://github.com/ecodeclub/ekit/pull/212) - [ekit: AnyValue 支持As[Type]类型 String 转换](https://github.com/ecodeclub/ekit/pull/213) - [stringx: unsafe 转换 string 和 []byte](https://github.com/ecodeclub/ekit/pull/215) + - [stringx: 添加 Benchmark](https://github.com/ecodeclub/ekit/pull/216) # v0.0.7 - [slice: FilterDelete](https://github.com/ecodeclub/ekit/pull/152) diff --git a/stringx/string_test.go b/stringx/string_test.go index a2f6ab2e..6f773813 100644 --- a/stringx/string_test.go +++ b/stringx/string_test.go @@ -126,3 +126,35 @@ func TestUnsafeToString(t *testing.T) { }) } } + +func Benchmark_UnsafeToBytes(b *testing.B) { + b.Run("safe to bytes", func(b *testing.B) { + s := "hello ekit! hello golang! this is test benchmark" + for i := 0; i < b.N; i++ { + _ = []byte(s) + } + }) + + b.Run("unsafe to bytes", func(b *testing.B) { + s := "hello ekit! hello golang! this is test benchmark" + for i := 0; i < b.N; i++ { + _ = UnsafeToBytes(s) + } + }) +} + +func Benchmark_UnsafeToString(b *testing.B) { + b.Run("safe to string", func(b *testing.B) { + s := []byte("hello ekit! hello golang! this is test benchmark") + for i := 0; i < b.N; i++ { + _ = string(s) + } + }) + + b.Run("unsafe to string", func(b *testing.B) { + s := []byte("hello ekit! hello golang! this is test benchmark") + for i := 0; i < b.N; i++ { + _ = UnsafeToString(s) + } + }) +} diff --git a/stringx/stringx_benchmark b/stringx/stringx_benchmark new file mode 100644 index 00000000..b5e83bcc --- /dev/null +++ b/stringx/stringx_benchmark @@ -0,0 +1,10 @@ +goos: darwin +goarch: amd64 +pkg: github.com/ecodeclub/ekit/stringx +cpu: Intel(R) Core(TM) i7-7920HQ CPU @ 3.10GHz +Benchmark_UnsafeToBytes/safe_to_bytes-8 39721614 29.60 ns/op 48 B/op 1 allocs/op +Benchmark_UnsafeToBytes/unsafe_to_bytes-8 1000000000 0.2805 ns/op 0 B/op 0 allocs/op +Benchmark_UnsafeToString/safe_to_string-8 45207981 26.77 ns/op 48 B/op 1 allocs/op +Benchmark_UnsafeToString/unsafe_to_string-8 1000000000 0.2842 ns/op 0 B/op 0 allocs/op +PASS +ok github.com/ecodeclub/ekit/stringx 4.780s From a2a10507a0c779b94ed93c734779e5c9daa15c14 Mon Sep 17 00:00:00 2001 From: Deng Ming Date: Mon, 25 Sep 2023 22:19:31 +0800 Subject: [PATCH 32/32] =?UTF-8?q?v0.0.8=20=E7=9A=84changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 91281f4f..2ffc5fdc 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,4 +1,6 @@ # 开发中 + +# v0.0.8 - [atomicx: 泛型封装 atomic.Value](https://github.com/gotomicro/ekit/pull/101) - [queue: API 定义](https://github.com/gotomicro/ekit/pull/109) - [queue: 基于堆和切片的优先级队列](https://github.com/gotomicro/ekit/pull/110)