Skip to content

Commit 6fdf3ad

Browse files
feat(spi): 新增spi包及其实现 (#258)
1 parent d28c6a4 commit 6fdf3ad

File tree

10 files changed

+327
-2
lines changed

10 files changed

+327
-2
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
with:
3131
go-version: 1.20.0
3232

33+
3334
- name: Build
3435
run: go build -v ./...
3536

.golangci.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@
1414

1515
run:
1616
go: '1.20'
17-
skip-dirs:
18-
- .idea
17+
issues:
18+
exclude-dirs:
19+
- .idea
20+
linters-settings:
21+
errcheck:
22+
ignore: ''

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.20
55
require (
66
github.com/DATA-DOG/go-sqlmock v1.5.0
77
github.com/mattn/go-sqlite3 v1.14.15
8+
github.com/pkg/errors v0.9.1
89
github.com/stretchr/testify v1.8.4
910
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
1011
golang.org/x/sync v0.4.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
1313
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
1414
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
1515
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
16+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
17+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1618
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
1719
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1820
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=

spi/spi.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2021 ecodeclub
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package spi
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"plugin"
22+
23+
"github.com/pkg/errors"
24+
)
25+
26+
// LoadService 加载 dir 下面的所有的实现了 T 接口的类型
27+
// 举个例子来说,如果你有一个叫做 UserService 的接口
28+
// 而后你将所有的实现都放到了 /ext/user_service 目录下
29+
// 并且所有的实现,虽然在不同的包,但是都叫做 UserService
30+
// 那么我可以执行 LoadService("/ext/user_service", "UserService")
31+
// 加载到所有的实现
32+
// LoadService 加载 dir 下面的所有的实现了 T 接口的类型
33+
34+
var (
35+
ErrDirNotFound = errors.New("ekit: 目录不存在")
36+
ErrSymbolNameIsEmpty = errors.New("ekit: 结构体名不能为空")
37+
ErrOpenPluginFailed = errors.New("ekit: 打开插件失败")
38+
ErrSymbolNameNotFound = errors.New("ekit: 从插件中查找对象失败")
39+
ErrInvalidSo = errors.New("ekit: 插件非该接口类型")
40+
)
41+
42+
func LoadService[T any](dir string, symName string) ([]T, error) {
43+
var services []T
44+
// 检查目录是否存在
45+
if _, err := os.Stat(dir); os.IsNotExist(err) {
46+
return nil, fmt.Errorf("%w", ErrDirNotFound)
47+
}
48+
if symName == "" {
49+
return nil, fmt.Errorf("%w", ErrSymbolNameIsEmpty)
50+
}
51+
// 遍历目录下的所有 .so 文件
52+
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
53+
if !info.IsDir() && filepath.Ext(path) == ".so" {
54+
// 打开插件
55+
p, err := plugin.Open(path)
56+
if err != nil {
57+
return fmt.Errorf("%w: %w", ErrOpenPluginFailed, err)
58+
}
59+
// 查找变量
60+
sym, err := p.Lookup(symName)
61+
if err != nil {
62+
return fmt.Errorf("%w: %w", ErrSymbolNameNotFound, err)
63+
}
64+
65+
// 尝试将符号断言为接口类型
66+
service, ok := sym.(T)
67+
if !ok {
68+
return fmt.Errorf("%w", ErrInvalidSo)
69+
}
70+
// 收集服务
71+
services = append(services, service)
72+
}
73+
return nil
74+
})
75+
return services, err
76+
}

spi/spi_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2021 ecodeclub
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package spi
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"os/exec"
21+
"path/filepath"
22+
"testing"
23+
24+
"github.com/stretchr/testify/require"
25+
"github.com/stretchr/testify/suite"
26+
27+
"github.com/stretchr/testify/assert"
28+
)
29+
30+
type LoadServiceSuite struct {
31+
suite.Suite
32+
}
33+
34+
func (l *LoadServiceSuite) SetupTest() {
35+
t := l.T()
36+
wd, err := os.Getwd()
37+
require.NoError(t, err)
38+
cmd := exec.Command("go", "generate", "./...")
39+
cmd.Dir = filepath.Join(wd, "testdata")
40+
output, err := cmd.CombinedOutput()
41+
require.NoError(t, err, fmt.Sprintf("执行 go generate 失败: %v\n%s", err, output))
42+
}
43+
44+
func (l *LoadServiceSuite) Test_LoadService() {
45+
t := l.T()
46+
testcases := []struct {
47+
name string
48+
dir string
49+
svcName string
50+
want []string
51+
assertFunc assert.ErrorAssertionFunc
52+
}{
53+
{
54+
name: "有一个插件",
55+
dir: "./testdata/user_service",
56+
svcName: "UserSvc",
57+
want: []string{"Get"},
58+
assertFunc: assert.NoError,
59+
},
60+
{
61+
name: "有两个插件",
62+
dir: "./testdata/user_service2",
63+
svcName: "UserSvc",
64+
want: []string{"A", "B"},
65+
assertFunc: assert.NoError,
66+
},
67+
{
68+
name: "目录不存在",
69+
dir: "./notfound",
70+
assertFunc: func(t assert.TestingT, err error, i ...interface{}) bool {
71+
return assert.ErrorIs(t, err, ErrDirNotFound)
72+
},
73+
},
74+
{
75+
name: "svcName为空",
76+
dir: "./testdata/user_service2",
77+
svcName: "",
78+
assertFunc: func(t assert.TestingT, err error, i ...interface{}) bool {
79+
return assert.ErrorIs(t, err, ErrSymbolNameIsEmpty)
80+
},
81+
},
82+
{
83+
name: "svcName没找到",
84+
dir: "./testdata/user_service2",
85+
svcName: "notfound",
86+
assertFunc: func(t assert.TestingT, err error, i ...interface{}) bool {
87+
return assert.ErrorIs(t, err, ErrSymbolNameNotFound)
88+
},
89+
},
90+
{
91+
name: "加载的对象未实现对应的抽象",
92+
dir: "./testdata/user_service3",
93+
svcName: "UserSvc",
94+
assertFunc: func(t assert.TestingT, err error, i ...interface{}) bool {
95+
return assert.ErrorIs(t, err, ErrInvalidSo)
96+
},
97+
},
98+
}
99+
for _, tc := range testcases {
100+
t.Run(tc.name, func(t *testing.T) {
101+
list, err := LoadService[UserService](tc.dir, tc.svcName)
102+
tc.assertFunc(t, err)
103+
if err != nil {
104+
return
105+
}
106+
ans := make([]string, 0, len(list))
107+
for _, svc := range list {
108+
ans = append(ans, svc.Get())
109+
}
110+
assert.Equal(t, tc.want, ans)
111+
})
112+
}
113+
}
114+
115+
func TestLoadServiceSuite(t *testing.T) {
116+
suite.Run(t, new(LoadServiceSuite))
117+
}
118+
119+
type UserService interface {
120+
Get() string
121+
}
122+
123+
func ExampleLoadService() {
124+
getters, err := LoadService[UserService]("./testdata/user_service", "UserSvc")
125+
fmt.Println(err)
126+
for _, getter := range getters {
127+
fmt.Println(getter.Get())
128+
}
129+
// Output:
130+
// <nil>
131+
// Get
132+
}

spi/testdata/user_service/a.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2021 ecodeclub
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package main
15+
16+
// 测试用
17+
//go:generate go build -race --buildmode=plugin -o a.so ./a.go
18+
19+
type UserService struct{}
20+
21+
func (u UserService) Get() string {
22+
return "Get"
23+
}
24+
25+
var UserSvc UserService

spi/testdata/user_service2/a/a.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2021 ecodeclub
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:generate go build -race --buildmode=plugin -o ../a.so ./a.go
16+
17+
package main
18+
19+
// 测试用
20+
21+
type UserService struct{}
22+
23+
// GetName returns the name of the service
24+
func (u UserService) Get() string {
25+
return "A"
26+
}
27+
28+
// 导出对象
29+
var UserSvc UserService

spi/testdata/user_service2/b/b.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2021 ecodeclub
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:generate go build -race --buildmode=plugin -o ../b.so ./b.go
16+
package main
17+
18+
// 测试用
19+
20+
type UserService struct{}
21+
22+
// GetName returns the name of the service
23+
func (u UserService) Get() string {
24+
return "B"
25+
}
26+
27+
// 导出对象
28+
var UserSvc UserService

spi/testdata/user_service3/a.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2021 ecodeclub
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
// 测试用
18+
19+
//go:generate go build -race --buildmode=plugin -o a.so ./a.go
20+
21+
type UserService struct{}
22+
23+
func (u UserService) GetV1() string {
24+
return "Get"
25+
}
26+
27+
var UserSvc UserService

0 commit comments

Comments
 (0)