背景
在用go操作mysql数据库时会经常见到类似下面的代码,空导入"go-sql-driver/mysql"
import (
"database/sql"
"time"
_ "github.com/go-sql-driver/mysql" // 空导入
)
// ...
db, err := sql.Open("mysql", "user:password@/dbname")
我之前对上面代码有点疑问:空导入有什么意义吗?
后面知道go在导入包时,会执行包中的init函数,所以上面的空导入会执行 github.com/go-sql-driver/mysql/driver.go[1] 中的init函数来注册驱动
func init() {
sql.Register("mysql", &MySQLDriver{})
}
再后来,见到好多次类似"注册模式"的写法,逐渐能从中体会到"面向接口编程"的思想。然后就多了一个好处:在做代码审计时,因为了解这种"业务套路",所以更容易理解代码逻辑;
下面分享三个用到这种模式的例子,分别是go sql库、go swagger库、python flask库
分析
这种"注册模式"是什么?
包含有三个角色:
-
接口层:定义接口、提供"注册实例接口"、提供"获取服务"或者"功能接口" -
服务提供者:实现接口、注册"接口实现" -
服务使用者:调用"获取服务接口"
这个结论是我根据三个例子总结出来的,下面来具体看看三个角色的功能
go sql
是什么?
见 https://github.com/go-sql-driver/mysql[2] 文档中的例子,用户可以使用驱动名获取到"包含驱动实例的对象"
db, err := sql.Open("mysql", "user:password@/dbname") // db中包含驱动实现
这里"用户"就是"服务使用者"。
"接口层"是"database/sql"库,它在 database/sql/driver/driver.go[3] 文件中定义了接口,"驱动"需要实现下面的Open方法
type Driver interface {
...
Open(name string) (Conn, error)
}
database/sql/sql.go[4]中提供"注册实例接口","驱动"可以调用Register函数注册。
func Register(name string, driver driver.Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("sql: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
drivers[name] = driver
}
database/sql/sql.go[5]中提供"获取服务接口","用户"可以调用Open函数获取DB实例
func Open(driverName, dataSourceName string) (*DB, error) {
driversMu.RLock()
driveri, ok := drivers[driverName]
driversMu.RUnlock()
...
return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}
"服务提供者"就是"驱动",这里就是 github.com/go-sql-driver/mysql[6]
它实现了Driver接口
type MySQLDriver struct{}
...
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
cfg, err := ParseDSN(dsn)
if err != nil {
return nil, err
}
c := &connector{
cfg: cfg,
}
return c.Connect(context.Background())
}
并且调用Register方法注册"接口实现",代码见github.com/go-sql-driver/mysql/driver.go[7]
func init() {
sql.Register("mysql", &MySQLDriver{})
}
swagger
是什么?
在审计 tidb-dashboard[8] 项目时,关注到swagger[9]。
tidb-dashboard用swagger来提供在线的api文档服务。
服务使用者
"服务使用者",这里是tidb-dashboard[10]
func Handler() http.Handler {
return httpSwagger.Handler()
}
如果跟进方法,就会看到调用swag.ReadDoc()
方法,这个方法就是"接口层"提供的"获取服务接口"。
接口层
"接口层"就是swagger库,在 swagger.go[11] 文件中
定义接口
// Swagger is an interface to read swagger document.
type Swagger interface {
ReadDoc() string
}
提供"注册实例接口"
// Register registers swagger for given name.
func Register(name string, swagger Swagger) {
...
swags[name] = swagger
}
提供"获取服务接口"
func ReadDoc(optionalName ...string) (string, error) {
...
swag, ok := swags[name]
...
return swag.ReadDoc(), nil
}
服务提供者
"服务提供者"这里是用户自己。这里用法有点特殊,tidb-dashboard仓库中没有"服务实现"相关代码,在编译tidb-dashboard项目时会生成代码。
生成后的代码我放在了gist[12]。
可以看到它实现了Swagger接口
type s struct{}
func (s *s) ReadDoc() string {
...
return tpl.String()
}
注册接口
func init() {
swag.Register(swag.Name, &s{})
}
和第一个例子的区别
区别在于,这个例子中,"服务提供者"和"服务使用者"都是用户自己。
那么为什么不直接自己调自己,还经过"接口层"呢?
python flask扩展
是什么?
用户可以用flask框架的cors扩展来做跨域请求时的限制。
插件文档[13]中的例子如下
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
@app.route("/")
def helloWorld():
return "Hello, cross-origin-world!"
这里"服务使用者"不需要找"接口层"要"服务实例"
接口层
在flask框架[14]中定义了一个函数AfterRequestCallable
类型
提供"注册实例接口",如下,只是把函数放进了列表中
@setupmethod
def after_request(self, f: AfterRequestCallable) -> AfterRequestCallable:
..
self.after_request_funcs.setdefault(None, []).append(f)
return f
服务提供者
cors插件在flask框架基础上,提供了cors相关的安全能力。
在 flask_cors/extension.py[15] 中
实现接口:cors_after_request函数是AfterRequestCallable
类型具体的实现
def make_after_request_function(resources):
def cors_after_request(resp):
...
normalized_path = unquote_plus(request.path)
for res_regex, res_options in resources:
if try_match(normalized_path, res_regex):
...
...
return resp
return cors_after_request
注册接口:在CORS实例化时,会注册实例提供安全能力
class CORS(object):
...
def __init__(self, app=None, **kwargs):
...
self.init_app(app, **kwargs)
def init_app(self, app, **kwargs):
...
cors_after_request = make_after_request_function(resources)
app.after_request(cors_after_request) # 有点注册一个中间件的感觉
和前面两个例子的区别
细品的话,可以看到这个例子和前两个例子又有很多不同:
-
没有显式的"接口定义",毕竟python没有接口关键字 -
注册的是"函数",而不是"对象" -
"接口层"不提供"获取服务","服务使用者"也不需要"获取服务" -
由"服务使用者"注册"接口实例",而不是"服务提供者"注册接口实例
可以想一想为什么会有这些区别,能把这些区别"修改"回去吗?比如如果我是cors扩展库作者,我就不能在cors库里自动注册服务,让库的使用者少写几行代码吗?
总结
通过分析这三个"注册模式"的例子,我自己对"面向接口编程"有点感觉。后面感觉这种思想很基础、很常见、很实用,比如rpc、spring ioc容器、微服务的服务注册等都和这个"注册模式"很像。
如果你觉得疑惑,或者觉得我写得比较怪,推荐你找一个你熟悉的库自己分析一下。
参考资料
github.com/go-sql-driver/mysql/driver.go: https://github.com/go-sql-driver/mysql/blob/v1.6.0/driver.go
[2]https://github.com/go-sql-driver/mysql: https://github.com/go-sql-driver/mysql
[3]database/sql/driver/driver.go: https://github.com/golang/go/blob/go1.17.9/src/database/sql/driver/driver.go
[4]database/sql/sql.go: https://github.com/golang/go/blob/go1.17.9/src/database/sql/sql.go
[5]database/sql/sql.go: https://github.com/golang/go/blob/go1.17.9/src/database/sql/sql.go
[6]github.com/go-sql-driver/mysql: https://github.com/go-sql-driver/mysql/blob/v1.6.0/driver.go
[7]github.com/go-sql-driver/mysql/driver.go: https://github.com/go-sql-driver/mysql/blob/v1.6.0/driver.go
[8]tidb-dashboard: https://github.com/pingcap/tidb-dashboard
[9]swagger: https://github.com/swaggo/swag
[10]tidb-dashboard: https://github.com/pingcap/tidb-dashboard/blob/v2022.03.31.1/pkg/swaggerserver/handler.go
[11]swagger.go: https://github.com/swaggo/swag/blob/v1.8.1/swagger.go
[12]gist: https://gist.github.com/leveryd/5ae3ce4940831464d43945f42c68b4c0
[13]插件文档: https://github.com/corydolphin/flask-cors
[14]flask框架: https://github.com/pallets/flask/blob/main/src/flask/scaffold.py
[15]flask_cors/extension.py: https://github.com/corydolphin/flask-cors/blob/master/flask_cors/extension.py
原文始发于微信公众号(leveryd):面向接口编程的三个例子
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论