Skip to content

Commit

Permalink
feat(spi): 新增spi包及其实现 (#258)
Browse files Browse the repository at this point in the history
  • Loading branch information
juniaoshaonian authored Jun 4, 2024
1 parent d28c6a4 commit 6fdf3ad
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 2 deletions.
1 change: 1 addition & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
with:
go-version: 1.20.0


- name: Build
run: go build -v ./...

Expand Down
8 changes: 6 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@

run:
go: '1.20'
skip-dirs:
- .idea
issues:
exclude-dirs:
- .idea
linters-settings:
errcheck:
ignore: ''
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
76 changes: 76 additions & 0 deletions spi/spi.go
Original file line number Diff line number Diff line change
@@ -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
}
132 changes: 132 additions & 0 deletions spi/spi_test.go
Original file line number Diff line number Diff line change
@@ -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:
// <nil>
// Get
}
25 changes: 25 additions & 0 deletions spi/testdata/user_service/a.go
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions spi/testdata/user_service2/a/a.go
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions spi/testdata/user_service2/b/b.go
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions spi/testdata/user_service3/a.go
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 6fdf3ad

Please sign in to comment.