详解Go语言如何对数据库进行CRUD操作

Go语言中的接口

在这里我将不会介绍接口基础知识,如:接口定义和实现、空接口、类型断言、v, ok := i.(T)、switch x.(type)、接口嵌套、指针接收器与值接收器实现接口、接口零值 这些基础概念不作多说,如不太清楚上面接口知识,自行学习补充。这里介绍在开发中接口实在用法,也是由上面基础演变而成。

使用接口编程

有这么一个普通场景,上传并保存文件,三七二十一蹭蹭蹭,一下子搞完,so easy,代码如下:

-1

项目准备上线,公司购买了OSS存储,文件资源要存到OSS上,那么如何修改上面的代码呢? 这个时候接口就可以上场了。 实现如下:

  1. // NOTE: storage.go
  2. // 存储文件接口
  3. type Storage interface {
  4.      Save(data []byte) (string, error)
  5. }
  6. // 开发时候使用本地存储
  7. type localStorage struct {
  8.      config *struct{ base string }
  9. }
  10. func NewLocalStorage(cfg struct{ base string }) *localStorage {
  11.      return &localStorage{config: &cfg}
  12. }
  13. func (store localStorage) Save(data []byte) (string, error) {
  14.      return “http://127.0.0.1/” + store.config.base + “/a.png”, nil
  15. }
  16. // 生产使用oss存储
  17. type ossStorage struct{}
  18. func NewOSSStorage() *ossStorage {
  19.      return &ossStorage{}
  20. }
  21. func (store ossStorage) Save(data []byte) (string, error) {
  22.      return “https://abc.com/oss/a.png”, nil
  23. }
  24. // NOTE: upload.go
  25. // 保存文件
  26. func saveFile(store Storage) {
  27.      var data []byte // 伪代码
  28.      url, _ := store.Save(data)
  29.      fmt.Println(url)
  30. }
  31. func main() {
  32.      var store Storage
  33.      if os.Getenv(“ENV”) == “prod” {
  34.          store = NewOSSStorage()
  35.      } else {
  36.          store = NewLocalStorage(struct{ base string }{base: “/static”})
  37.      }
  38.      saveFile(store)
  39. }

上面定义Storage接口,localStorage、ossStorage 都是实现了接口,而 saveFile(store Storage) 接受指针。 这种方式就是接受接口返回结构,是 Go 语言接口编程中非常经典实用模式。

接受接口返回结构

上面已经的例子就是使用这个法则,使用这个法则让代码变得更加灵活,它是松耦合的,更加方便mock数据测试,可以避免一些不必要的bug。

现在举一个不方便测试的例子:

  1. type Repository struct {
  2.      DB *sql.DB
  3. }
  4. func NewRepository(db *sql.DB) *Repository {
  5.      return &Repository{DB: db}
  6. }
  7. func (repo *Repository) Insert() {
  8.      // do some thing
  9. }
  10. func (repo *Repository) Update() {
  11.      // do some thing
  12. }
  13. type Service struct {
  14.      Repo Repository
  15. }
  16. func NewService(db *sql.DB) *Service {
  17.      return &Service{
  18.          Repo: *NewRepository(db),
  19.      }
  20. }
  21. func main() {
  22.      // 假设是真实的链接 &sql.DB{}
  23.      srv := NewService(&sql.DB{})
  24.      srv.Repo.Insert()
  25.      srv.Repo.Update()
  26. }

上面的例子看着无任何问题啊?这不很正常。是的很正常,就是测试的时候不方便。 如果要测试起来,你必须要创建一个数据库,连接数据库,创建sql.DB实例。才能完成测试。如果想简单mock数据,就不好办了。

因此还是建议,使用接口编程。修改如下:

  1. type Repository interface {
  2.      Insert()
  3.      Update()
  4. }
  5.  
  6. type repository struct {
  7.      DB *sql.DB
  8. }
  9.  
  10. func NewRepository(db *sql.DB) *repository {
  11.      return &repository{DB: db}
  12. }
  13.  
  14. func (repo *repository) Insert() {
  15.      // do some thing
  16. }
  17.  
  18. func (repo *repository) Update() {
  19.      // do some thing
  20. }
  21.  
  22. type Service struct {
  23.      Repo Repository
  24. }
  25.  
  26. func NewService(Repository) *Service {
  27.      return &Service{
  28.          Repo: r,
  29.      }
  30. }
  31.  
  32. func main() {
  33.      // 假设是真实的链接 &sql.DB{}
  34.      r := NewRepository(&sql.DB{})
  35.      srv := NewService(r)
  36.      srv.Repo.Insert()
  37.      srv.Repo.Update()
  38. }

接口检查

上面的例子,func NewRepository(db *sql.DB) *repository 返回的是结构体指针。在编码过程,很容易忘记实现接口,甚至全部接口都没实现,编译器也不会报错。我怎么知道repository结构体就一定全部实现了Repository接口呢,对吧。

那这个时候就要用到接口检查了。 接口检查的语法是

var _ MyInterface = (*MyStruct)(nil)

-2

上面当我没有实现Inser方法时就会直接编译不通过。

这就是接口检查带来好处。

扩展接口

假设有这么一场景,在开发中我们使用到了第三方库,第三库提供一个Worker接口,我们在work、next两个方法都使用到Worker接口,入参就是它。这个时候我们希望在work函数中对Context增加额外的属性,提供给next函数使用。但现实是没法直接通过context.WithValue(w.Context(), "greet", "Hello")设置值的,因为Worker接口仅返回Context并重新没有设置Context方法提供;由于Worker是第三方的接口,不可直接去改的。因此就需要扩展接口。问题如图:

-3

如何解决这个问题呢?现在要解决什么问题?如果把问题细分的话,要解决2个问题:

  • 扩展一个设置Context的方法
  • 保留原来的方法,只做扩充,不做删除 那要解决这个问题最好的方式就是接口嵌套。

代码该如何写呢?解法如下:

首先是要定义新的接口wrapWorker,将旧的接口嵌套进来,这样就保留了原始有的方法,然后在定义新的方法SetContext(context.Context),接口定义好了,就用实现接口对吧,思路很直接。

紧接着定义一个 wrap 结构体, 保存ctx,同时实SetContext(context.Context)

还有重要的一环节,就是重写 Context() context.Context 方法。

实现代码如下:

  1. // 第三方的接口定义
  2. type Worker interface {
  3.      Context() context.Context
  4.      DOSomeThing()
  5.      // …
  6. }
  7.  
  8. type wrapWorker interface {
  9.      Worker
  10.      SetContext(context.Context)
  11. }
  12. type wrap struct {
  13.      Worker
  14.      ctx context.Context
  15. }
  16.  
  17. func newWrap(Worker) wrapWorker {
  18.      return &wrap{
  19.          w,
  20.          w.Context(),
  21.      }
  22. }
  23. func (wp *wrap) SetContext(ctx context.Context) {
  24.      wp.ctx = ctx
  25. }
  26.  
  27. func (wp wrap) Context() context.Context {
  28.      return wp.ctx
  29. }
  30.  
  31. type contextKey string
  32.  
  33. func work(Worker) {
  34.      wp := newWrap(w)
  35.      ctx := context.WithValue(w.Context(), contextKey(“greet”), “Hello”)
  36.      wp.SetContext(ctx)
  37.  
  38.      next(wp)
  39. }
  40.  
  41. func next(Worker) {
  42.      v := w.Context().Value(contextKey(“greet”))
  43.      fmt.Printf(“-> %v \n”, v)
  44.      w.DoSomeThing()
  45. }
  46.  
  47. type person string
  48.  
  49. func (person) Context() context.Context {
  50.      return context.Background()
  51. }
  52. func (p person) DoSomeThing() {
  53.      fmt.Println(string(p), “吃饭睡觉打代码~”)
  54. }
  55.  
  56. func main() {
  57.      var p person = “张三”
  58.      work(p)
  59. }

执行结果符合预期,OK 没问题~

-4

其实上面不定义新接口wrapWorker也是可以的,用wrap包裹接口即可。用接口设计更符合接口编程思想。

到此这篇关于详解Go语言如何对数据库进行CRUD操作的文章就介绍到这了,更多相关Go语言数据库CRUD操作内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

标签

发表评论