From 59061081c2ba99492ec26ba57abc1bda6397c3f9 Mon Sep 17 00:00:00 2001 From: tiendc Date: Sun, 21 Sep 2025 18:25:39 +0700 Subject: [PATCH] Fix: use exceptional copy method for Go standard types --- .github/workflows/go.yml | 16 +++---- .golangci.yaml | 87 ++++++++++++++++++++++++++++++++++++ .golangci.yml | 73 ------------------------------ Makefile | 2 +- build_copier.go | 35 ++++++++++++++- go.mod | 2 +- go.sum | 4 +- struct_copier_go1.23_test.go | 62 +++++++++++++++++++++++++ struct_copier_test.go | 47 +++++++++++++++++++ 9 files changed, 241 insertions(+), 87 deletions(-) create mode 100644 .golangci.yaml delete mode 100644 .golangci.yml create mode 100644 struct_copier_go1.23_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6fae7f2..37e3345 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -13,19 +13,19 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: ["1.18.x", "1.22.x", "1.23.x"] + go: ["1.18.x", "1.23.x", "1.25.x"] include: - - go: 1.23.x + - go: 1.18.x latest: true steps: - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Load cached dependencies uses: actions/cache@v4 @@ -35,11 +35,11 @@ jobs: restore-keys: | ${{ runner.os }}-go- - - name: Download Dependencies - run: make prepare - - name: Lint - run: make lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.4.0 + args: --timeout=3m -v - name: Test run: make cover diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..da70ccc --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,87 @@ +version: "2" +linters: + enable: + - bodyclose + - contextcheck + - copyloopvar + - dogsled + - err113 + - errname + - errorlint + - exhaustive + - forbidigo + - forcetypeassert + - funlen + - gocognit + - goconst + - gocritic + - gocyclo + - gosec + - lll + - misspell + - mnd + - nakedret + - nestif + - nilerr + - rowserrcheck + - staticcheck + - unconvert + - unparam + - whitespace + settings: + funlen: + lines: 120 + statements: 80 + gocognit: + min-complexity: 30 + gocyclo: + min-complexity: 30 + lll: + line-length: 120 + misspell: + locale: US + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - contextcheck + - err113 + - forcetypeassert + - funlen + - gocognit + - gocyclo + - gosec + - mnd + - staticcheck + - unused + - wrapcheck + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - goimports + settings: + gci: + sections: + - standard + - default + - prefix(github.com/tiendc/go-deepcopy) + goimports: + local-prefixes: + - github.com/golangci/golangci-lint + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index 717880b..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,73 +0,0 @@ -linters-settings: - funlen: - lines: 120 - statements: 80 - gci: - sections: - - standard - - default - - prefix(github.com/tiendc/go-deepcopy) - gocyclo: - min-complexity: 20 - goimports: - local-prefixes: github.com/golangci/golangci-lint - lll: - line-length: 120 - misspell: - locale: US - -linters: - enable: - - bodyclose - - contextcheck - - dogsled - - errcheck - - errname - - errorlint - - exhaustive - - copyloopvar - - forbidigo - - forcetypeassert - - funlen - - gci - - gocognit - - goconst - - gocritic - - gocyclo - - err113 - - gofmt - - goimports - - mnd - - gosec - - gosimple - - govet - - ineffassign - - lll - - misspell - - nakedret - - nestif - - nilerr - - rowserrcheck - - staticcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - whitespace - -issues: - exclude-rules: - - path: _test\.go - linters: - - funlen - - contextcheck - - staticcheck - - gocyclo - - gocognit - - err113 - - forcetypeassert - - wrapcheck - - gomnd - - errorlint - - unused \ No newline at end of file diff --git a/Makefile b/Makefile index b81dafa..c82822e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ all: lint test prepare: - @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.60.3 + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.4.0 build: @go build -v ./... diff --git a/build_copier.go b/build_copier.go index 343b6ca..5800b55 100644 --- a/build_copier.go +++ b/build_copier.go @@ -3,6 +3,7 @@ package deepcopy import ( "fmt" "reflect" + "strings" "sync" ) @@ -171,8 +172,14 @@ func buildCopier(ctx *Context, dstType, srcType reflect.Type) (copier copier, er //nolint:nestif if srcKind == reflect.Struct { if dstKind == reflect.Struct { - // At this point, cachedCopier should be `nil`. - // If it's non-nil, seems like a circular reference occurs, use an inline copier. + // Build a special copier for Go standard types such as time.Time, unique.Handle + copier = buildCopierForStandardStructs(dstType, srcType) + if copier != nil { + goto OnComplete + } + + // At this point, cached copier should not exist in the cache map. + // If it exists, seems like a circular reference occurs, use an inline copier. if cachedCopierFound { return &inlineCopier{ctx: ctx, dstType: dstType, srcType: srcType}, nil } @@ -226,6 +233,30 @@ OnNonCopyable: return nil, fmt.Errorf("%w: %v -> %v", ErrTypeNonCopyable, srcType, dstType) } +func buildCopierForStandardStructs(dstType, srcType reflect.Type) copier { + switch srcType.PkgPath() { + // When copy time.Time -> time.Time or derived type + case "time": + if dstType == srcType { + return defaultDirectCopier + } + if dstType.ConvertibleTo(srcType) { + return defaultConvCopier + } + // When copy unique.Handle[T] -> unique.Handle[T] or derived type + case "unique": + if strings.HasPrefix(srcType.Name(), "Handle[") { + if dstType == srcType { + return defaultDirectCopier + } + if dstType.ConvertibleTo(srcType) { + return defaultConvCopier + } + } + } + return nil +} + func setCachedCopier(ctx *Context, cacheKey *cacheKey, cp copier) { ctx.mu.Lock() ctx.copierCacheMap[*cacheKey] = cp diff --git a/go.mod b/go.mod index 9faa9a7..68aefba 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/tiendc/go-deepcopy go 1.18 -require github.com/stretchr/testify v1.9.0 +require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 60ce688..c4c1710 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/struct_copier_go1.23_test.go b/struct_copier_go1.23_test.go new file mode 100644 index 0000000..06b5d20 --- /dev/null +++ b/struct_copier_go1.23_test.go @@ -0,0 +1,62 @@ +//go:build go1.23 + +package deepcopy + +import ( + "testing" + "unique" + + "github.com/stretchr/testify/assert" +) + +func Test_Copy_struct_on_standard_types_go1_23(t *testing.T) { + t.Run("#1: Copy unique.Handle[string]", func(t *testing.T) { + s := unique.Make("hello") + var d unique.Handle[string] + err := Copy(&d, s) + assert.Nil(t, err) + assert.Equal(t, d, s) + assert.Equal(t, d.Value(), s.Value()) + + // unique.Handle[T] as struct fields + type A struct { + I int + } + type S struct { + H unique.Handle[A] + } + type D struct { + H unique.Handle[A] + } + s2 := S{H: unique.Make(A{I: 111})} + var d2 D + err = Copy(&d2, s2) + assert.Nil(t, err) + assert.Equal(t, d2.H, s2.H) + assert.Equal(t, d2.H.Value(), s2.H.Value()) + }) + + t.Run("#2: Copy unique.Handle[string] to derived type", func(t *testing.T) { + type T unique.Handle[string] + s := unique.Make("hello") + var d T + err := Copy(&d, s) + assert.Nil(t, err) + assert.Equal(t, unique.Handle[string](d), s) + assert.Equal(t, unique.Handle[string](d).Value(), s.Value()) + + // unique.Handle[T] as struct fields + type S struct { + H unique.Handle[string] + } + type D struct { + H T + } + s2 := S{H: unique.Make("hello")} + var d2 D + err = Copy(&d2, s2) + assert.Nil(t, err) + assert.Equal(t, unique.Handle[string](d2.H), s2.H) + assert.Equal(t, unique.Handle[string](d2.H).Value(), s2.H.Value()) + }) +} diff --git a/struct_copier_test.go b/struct_copier_test.go index 3cff762..b0f256a 100644 --- a/struct_copier_test.go +++ b/struct_copier_test.go @@ -1278,3 +1278,50 @@ func Test_Copy_struct_with_post_copy_event(t *testing.T) { assert.Equal(t, testD25{I: 1, S: "a"}, d) // won't work }) } + +func Test_Copy_struct_on_standard_types(t *testing.T) { + t.Run("#1: Copy time.Time to time.Time", func(t *testing.T) { + s := time.Now() + var d time.Time + err := Copy(&d, s) + assert.Nil(t, err) + assert.Equal(t, d, s) + + // time.Time as struct fields + type S struct { + T time.Time + } + type D struct { + T time.Time + } + s2 := S{T: time.Now()} + var d2 D + err = Copy(&d2, s2) + assert.Nil(t, err) + assert.Equal(t, d2.T, s2.T) + }) + + t.Run("#2: Copy time.Time to derived type", func(t *testing.T) { + type T time.Time + s := time.Now() + var d T + err := Copy(&d, s) + assert.Nil(t, err) + assert.Equal(t, time.Time(d), s) + + // time.Time as struct fields + type S struct { + T time.Time + } + type D struct { + T T + } + s2 := S{T: time.Now()} + var d2 D + err = Copy(&d2, s2) + assert.Nil(t, err) + assert.Equal(t, time.Time(d2.T), s2.T) + }) + + // NOTE: unique.Handle[T] is available from Go1.23, it is tested in the relevant test file +}