03-Go语言接口编程最佳方式、错误处理思想与Book表CRUD - 1
在这一小节,来学习在Go语言中如何对数据库进行CRUD操作,探讨一下Go的接口编程,在Go语开源项目中,优秀的项目都是通过接口编程模式,这也符合Go编程思想(大部分编程语言的思想),另外还学习如何处理错误,最后实践数据CRUD操作。
废话不多说,上链接~
代码仓库在这👇👇👇重要的事情说三遍
EBook 代码仓库 记得给个Star😘😘😘
代码仓库在这👇👇👇重要的事情说三遍
EBook 代码仓库 记得给个Star😍😍😍
在实际编码之前,先聊聊接口(interface
)和错误处理(Error Handling
)。
Go语言中的接口
在这里我将不会介绍接口基础知识,如:接口定义和实现、空接口、类型断言、v, ok := i.(T)、switch x.(type)、接口嵌套、指针接收器与值接收器实现接口、接口零值
这些基础概念不作多说,如不太清楚上面接口知识,自行学习补充。这里介绍在开发中接口实在用法,也是由上面基础演变而成。
使用接口编程
有这么一个普通场景,上传并保存文件,三七二十一蹭蹭蹭,一下子搞完,so easy,代码如下:
项目准备上线,公司购买了OSS存储,文件资源要存到OSS上,那么如何修改上面的代码呢?
这个时候接口就可以上场了。
实现如下:
// NOTE: storage.go
// 存储文件接口
type Storage interface {
Save(data []byte) (string, error)
}
// 开发时候使用本地存储
type localStorage struct {
config *struct{ base string }
}
func NewLocalStorage(cfg struct{ base string }) *localStorage {
return &localStorage{config: &cfg}
}
func (store localStorage) Save(data []byte) (string, error) {
return "http://127.0.0.1/" + store.config.base + "/a.png", nil
}
// 生产使用oss存储
type ossStorage struct{}
func NewOSSStorage() *ossStorage {
return &ossStorage{}
}
func (store ossStorage) Save(data []byte) (string, error) {
return "https://abc.com/oss/a.png", nil
}
// NOTE: upload.go
// 保存文件
func saveFile(store Storage) {
var data []byte // 伪代码
url, _ := store.Save(data)
fmt.Println(url)
}
func main() {
var store Storage
if os.Getenv("ENV") == "prod" {
store = NewOSSStorage()
} else {
store = NewLocalStorage(struct{ base string }{base: "/static"})
}
saveFile(store)
}
上面定义Storage
接口,localStorage、ossStorage 都是实现了接口,而 saveFile(store Storage)
接受指针。
这种方式就是接受接口返回结构
,是 Go 语言接口编程中非常经典实用模式。
接受接口返回结构
上面已经的例子就是使用这个法则,使用这个法则让代码变得更加灵活,它是松耦合的,更加方便mock数据测试,可以避免一些不必要的bug。
现在举一个不方便测试的例子:
type Repository struct {
DB *sql.DB
}
func NewRepository(db *sql.DB) *Repository {
return &Repository{DB: db}
}
func (repo *Repository) Insert() {
// do some thing
}
func (repo *Repository) Update() {
// do some thing
}
type Service struct {
Repo Repository
}
func NewService(db *sql.DB) *Service {
return &Service{
Repo: *NewRepository(db),
}
}
func main() {
// 假设是真实的链接 &sql.DB{}
srv := NewService(&sql.DB{})
srv.Repo.Insert()
srv.Repo.Update()
}
上面的例子看着无任何问题啊?这不很正常。是的很正常,就是测试的时候不方便。
如果要测试起来,你必须要创建一个数据库,连接数据库,创建sql.DB
实例。才能完成测试。如果想简单mock数据,就不好办了。
因此还是建议,使用接口编程。修改如下:
type Repository interface {
Insert()
Update()
}
type repository struct {
DB *sql.DB
}
func NewRepository(db *sql.DB) *repository {
return &repository{DB: db}
}
func (repo *repository) Insert() {
// do some thing
}
func (repo *repository) Update() {
// do some thing
}
type Service struct {
Repo Repository
}
func NewService(r Repository) *Service {
return &Service{
Repo: r,
}
}
func main() {
// 假设是真实的链接 &sql.DB{}
r := NewRepository(&sql.DB{})
srv := NewService(r)
srv.Repo.Insert()
srv.Repo.Update()
}
接口检查
上面的例子,func NewRepository(db *sql.DB) *repository
返回的是结构体指针。在编码过程,很容易忘记实现接口,甚至全部接口都没实现,编译器也不会报错。我怎么知道repository
结构体就一定全部实现了Repository
接口呢,对吧。
那这个时候就要用到接口检查了。 接口检查的语法是
var _ MyInterface = (*MyStruct)(nil)
上面当我没有实现Inser方法时就会直接编译不通过。
这就是接口检查带来好处。
扩展接口
假设有这么一场景,在开发中我们使用到了第三方库,第三库提供一个Worker
接口,我们在work、next
两个方法都使用到Worker
接口,入参就是它。这个时候我们希望在work函数中对Context
增加额外的属性,提供给next
函数使用。但现实是没法直接通过context.WithValue(w.Context(), "greet", "Hello")
设置值的,因为Worker
接口仅返回Context
并重新没有设置Context
方法提供;由于Worker
是第三方的接口,不可直接去改的。因此就需要扩展接口。问题如图:
如何解决这个问题呢?现在要解决什么问题?如果把问题细分的话,要解决2个问题:
-
- 扩展一个设置Context的方法
-
- 保留原来的方法,只做扩充,不做删除
那要解决这个问题最好的方式就是
接口嵌套
。
- 保留原来的方法,只做扩充,不做删除
那要解决这个问题最好的方式就是
代码该如何写呢?
解法如下:
首先是要定义新的接口wrapWorker
,将旧的接口嵌套进来,这样就保留了原始有的方法,然后在定义新的方法SetContext(context.Context)
,接口定义好了,就用实现接口对吧,思路很直接。
紧接着定义一个 wrap
结构体, 保存ctx,同时实SetContext(context.Context)
还有重要的一环节,就是重写 Context() context.Context
方法。
实现代码如下:
// 第三方的接口定义
type Worker interface {
Context() context.Context
DoSomeThing()
// ...
}
type wrapWorker interface {
Worker
SetContext(context.Context)
}
type wrap struct {
Worker
ctx context.Context
}
func newWrap(w Worker) wrapWorker {
return &wrap{
w,
w.Context(),
}
}
func (wp *wrap) SetContext(ctx context.Context) {
wp.ctx = ctx
}
func (wp wrap) Context() context.Context {
return wp.ctx
}
type contextKey string
func work(w Worker) {
wp := newWrap(w)
ctx := context.WithValue(w.Context(), contextKey("greet"), "Hello")
wp.SetContext(ctx)
next(wp)
}
func next(w Worker) {
v := w.Context().Value(contextKey("greet"))
fmt.Printf("-> %v \n", v)
w.DoSomeThing()
}
type person string
func (person) Context() context.Context {
return context.Background()
}
func (p person) DoSomeThing() {
fmt.Println(string(p), "吃饭睡觉打代码~")
}
func main() {
var p person = "张三"
work(p)
}
执行结果符合预期,OK 没问题~
其实上面不定义新接口wrapWorker
也是可以的,用wrap
包裹接口即可。用接口设计更符合接口编程思想。
好了这节就先分享到这里,下一节再来实现后续内容。
如果觉得我写得不错,记得点赞、关注、收藏
谢谢阅读~🤣🤣🤣
转载自:https://juejin.cn/post/7235237072440180791