From 6fdf3ad42c4b3123f7ea68acf5ed9e39f9b31276 Mon Sep 17 00:00:00 2001 From: wenliang zhu <73632785+juniaoshaonian@users.noreply.github.com> Date: Tue, 4 Jun 2024 09:51:19 +0800 Subject: [PATCH] =?UTF-8?q?=20feat(spi):=20=20=E6=96=B0=E5=A2=9Espi?= =?UTF-8?q?=E5=8C=85=E5=8F=8A=E5=85=B6=E5=AE=9E=E7=8E=B0=20(#258)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/go.yml | 1 + .golangci.yml | 8 +- go.mod | 1 + go.sum | 2 + spi/spi.go | 76 +++++++++++++++++ spi/spi_test.go | 132 ++++++++++++++++++++++++++++++ spi/testdata/user_service/a.go | 25 ++++++ spi/testdata/user_service2/a/a.go | 29 +++++++ spi/testdata/user_service2/b/b.go | 28 +++++++ spi/testdata/user_service3/a.go | 27 ++++++ 10 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 spi/spi.go create mode 100644 spi/spi_test.go create mode 100644 spi/testdata/user_service/a.go create mode 100644 spi/testdata/user_service2/a/a.go create mode 100644 spi/testdata/user_service2/b/b.go create mode 100644 spi/testdata/user_service3/a.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 026fc62b..0a73668e 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -30,6 +30,7 @@ jobs: with: go-version: 1.20.0 + - name: Build run: go build -v ./... diff --git a/.golangci.yml b/.golangci.yml index de3b12b8..26ce60be 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,5 +14,9 @@ run: go: '1.20' - skip-dirs: - - .idea \ No newline at end of file +issues: + exclude-dirs: + - .idea +linters-settings: + errcheck: + ignore: '' \ No newline at end of file diff --git a/go.mod b/go.mod index bce2f00a..d01a49ab 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/mattn/go-sqlite3 v1.14.15 + github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/sync v0.4.0 diff --git a/go.sum b/go.sum index 010d4b3e..0f76ae28 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= diff --git a/spi/spi.go b/spi/spi.go new file mode 100644 index 00000000..84c8423c --- /dev/null +++ b/spi/spi.go @@ -0,0 +1,76 @@ +// 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 spi + +import ( + "fmt" + "os" + "path/filepath" + "plugin" + + "github.com/pkg/errors" +) + +// LoadService 加载 dir 下面的所有的实现了 T 接口的类型 +// 举个例子来说,如果你有一个叫做 UserService 的接口 +// 而后你将所有的实现都放到了 /ext/user_service 目录下 +// 并且所有的实现,虽然在不同的包,但是都叫做 UserService +// 那么我可以执行 LoadService("/ext/user_service", "UserService") +// 加载到所有的实现 +// LoadService 加载 dir 下面的所有的实现了 T 接口的类型 + +var ( + ErrDirNotFound = errors.New("ekit: 目录不存在") + ErrSymbolNameIsEmpty = errors.New("ekit: 结构体名不能为空") + ErrOpenPluginFailed = errors.New("ekit: 打开插件失败") + ErrSymbolNameNotFound = errors.New("ekit: 从插件中查找对象失败") + ErrInvalidSo = errors.New("ekit: 插件非该接口类型") +) + +func LoadService[T any](dir string, symName string) ([]T, error) { + var services []T + // 检查目录是否存在 + if _, err := os.Stat(dir); os.IsNotExist(err) { + return nil, fmt.Errorf("%w", ErrDirNotFound) + } + if symName == "" { + return nil, fmt.Errorf("%w", ErrSymbolNameIsEmpty) + } + // 遍历目录下的所有 .so 文件 + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() && filepath.Ext(path) == ".so" { + // 打开插件 + p, err := plugin.Open(path) + if err != nil { + return fmt.Errorf("%w: %w", ErrOpenPluginFailed, err) + } + // 查找变量 + sym, err := p.Lookup(symName) + if err != nil { + return fmt.Errorf("%w: %w", ErrSymbolNameNotFound, err) + } + + // 尝试将符号断言为接口类型 + service, ok := sym.(T) + if !ok { + return fmt.Errorf("%w", ErrInvalidSo) + } + // 收集服务 + services = append(services, service) + } + return nil + }) + return services, err +} diff --git a/spi/spi_test.go b/spi/spi_test.go new file mode 100644 index 00000000..dc4eec71 --- /dev/null +++ b/spi/spi_test.go @@ -0,0 +1,132 @@ +// 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 spi + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/stretchr/testify/assert" +) + +type LoadServiceSuite struct { + suite.Suite +} + +func (l *LoadServiceSuite) SetupTest() { + t := l.T() + wd, err := os.Getwd() + require.NoError(t, err) + cmd := exec.Command("go", "generate", "./...") + cmd.Dir = filepath.Join(wd, "testdata") + output, err := cmd.CombinedOutput() + require.NoError(t, err, fmt.Sprintf("执行 go generate 失败: %v\n%s", err, output)) +} + +func (l *LoadServiceSuite) Test_LoadService() { + t := l.T() + testcases := []struct { + name string + dir string + svcName string + want []string + assertFunc assert.ErrorAssertionFunc + }{ + { + name: "有一个插件", + dir: "./testdata/user_service", + svcName: "UserSvc", + want: []string{"Get"}, + assertFunc: assert.NoError, + }, + { + name: "有两个插件", + dir: "./testdata/user_service2", + svcName: "UserSvc", + want: []string{"A", "B"}, + assertFunc: assert.NoError, + }, + { + name: "目录不存在", + dir: "./notfound", + assertFunc: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.ErrorIs(t, err, ErrDirNotFound) + }, + }, + { + name: "svcName为空", + dir: "./testdata/user_service2", + svcName: "", + assertFunc: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.ErrorIs(t, err, ErrSymbolNameIsEmpty) + }, + }, + { + name: "svcName没找到", + dir: "./testdata/user_service2", + svcName: "notfound", + assertFunc: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.ErrorIs(t, err, ErrSymbolNameNotFound) + }, + }, + { + name: "加载的对象未实现对应的抽象", + dir: "./testdata/user_service3", + svcName: "UserSvc", + assertFunc: func(t assert.TestingT, err error, i ...interface{}) bool { + return assert.ErrorIs(t, err, ErrInvalidSo) + }, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + list, err := LoadService[UserService](tc.dir, tc.svcName) + tc.assertFunc(t, err) + if err != nil { + return + } + ans := make([]string, 0, len(list)) + for _, svc := range list { + ans = append(ans, svc.Get()) + } + assert.Equal(t, tc.want, ans) + }) + } +} + +func TestLoadServiceSuite(t *testing.T) { + suite.Run(t, new(LoadServiceSuite)) +} + +type UserService interface { + Get() string +} + +func ExampleLoadService() { + getters, err := LoadService[UserService]("./testdata/user_service", "UserSvc") + fmt.Println(err) + for _, getter := range getters { + fmt.Println(getter.Get()) + } + // Output: + // + // Get +} diff --git a/spi/testdata/user_service/a.go b/spi/testdata/user_service/a.go new file mode 100644 index 00000000..64644e79 --- /dev/null +++ b/spi/testdata/user_service/a.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 main + +// 测试用 +//go:generate go build -race --buildmode=plugin -o a.so ./a.go + +type UserService struct{} + +func (u UserService) Get() string { + return "Get" +} + +var UserSvc UserService diff --git a/spi/testdata/user_service2/a/a.go b/spi/testdata/user_service2/a/a.go new file mode 100644 index 00000000..9916450f --- /dev/null +++ b/spi/testdata/user_service2/a/a.go @@ -0,0 +1,29 @@ +// 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. + +//go:generate go build -race --buildmode=plugin -o ../a.so ./a.go + +package main + +// 测试用 + +type UserService struct{} + +// GetName returns the name of the service +func (u UserService) Get() string { + return "A" +} + +// 导出对象 +var UserSvc UserService diff --git a/spi/testdata/user_service2/b/b.go b/spi/testdata/user_service2/b/b.go new file mode 100644 index 00000000..70657fd2 --- /dev/null +++ b/spi/testdata/user_service2/b/b.go @@ -0,0 +1,28 @@ +// 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. + +//go:generate go build -race --buildmode=plugin -o ../b.so ./b.go +package main + +// 测试用 + +type UserService struct{} + +// GetName returns the name of the service +func (u UserService) Get() string { + return "B" +} + +// 导出对象 +var UserSvc UserService diff --git a/spi/testdata/user_service3/a.go b/spi/testdata/user_service3/a.go new file mode 100644 index 00000000..bda44041 --- /dev/null +++ b/spi/testdata/user_service3/a.go @@ -0,0 +1,27 @@ +// 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 main + +// 测试用 + +//go:generate go build -race --buildmode=plugin -o a.so ./a.go + +type UserService struct{} + +func (u UserService) GetV1() string { + return "Get" +} + +var UserSvc UserService