如何用interface

一、了解下interface的相关知识

1、interface是一种类型

1type I interface {
2    Get() int
3}

首先 interface 是一种类型,从它的定义可以看出来用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。

go 允许不带任何方法的 interface ,这种类型的 interface 叫 empty interface(空接口)。

如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface,所以所有类型都实现了 empty interface,因为任何一种类型至少实现了 0 个方法。go 没有显式的关键字用来实现 interface,只需要实现 interface 包含的方法即可。

1type I interface {
2    Get() int
3}
4type i struct{
5    age int
6}
7func (ii *i) Get()int {
8    return ii.age
9}

上述例子中,i结构体有一个方法func (ii *i) Get()int

而接口I中只有一个方法Get() int,因为i结构体实现了接口I的所有方法,我们就称i结构体实现了接口I

2、interface 变量存储的是实现者的值

1type I interface {    
2    Get() int
3    Set(int)
4}
5
6type Dog struct {
7    age int
8}
9
10func(d *Dog) Get()int {
11    return d.age
12}
13
14type i struct{
15    age int
16}
17func (ii *i) Get()int {
18    return ii.age
19}
20
21func f(i I){
22    fmt.Println(i.Get())
23}
24
25func main() {
26    d := Dog{
27        age : 1,
28    } 
29    ii := i{
30        age : 10,
31    }
32    f(&d)  //1
33    f(&ii) //10
34}

函数f的参数是接口I,而Dog结构体和i结构体都实现了接口I,所以他们都可以被传入函数f中

不难看出两次调用f,分别输出的是d和ii的age字段

在使用 interface 时不需要显式在 struct 上声明要实现哪个 interface ,只需要实现对应 interface 中的方法即可,go 会自动进行 interface 的检查,并在运行时执行从其他类型到 interface 的自动转换


大概了解这些就足够了,如果你想更加了解interface的用法,可以自行搜索

接下来,我们来聊聊接口具体要如何使用


二、接口如何正确地使用

Producers and Consumers(生产者和消费者)

首先,这句话适用于哪里?这一切都归结为生产者包和消费者包之间的交互。生产者提供一些服务,消费者使用它。这种交互很常见,因为我们通常将代码组织到不同职责的包中。然后,包使用者将依赖于外部包来实现某些功能。

我们将给一个简单的演示来贯穿全文

├── db │ └── db.go └── user └── user.go

db包中, db.go提供了一些持久存储功能。在user包中, user.go包含一些我们想要与用户处理的业务逻辑。在这里, user包将成为consumer,使用db包提供的有状态服务。

Let the consumer define the interfaces it uses(让消费者定义其使用的接口)

也就是说,接口应该由consumer来定义,而不是producer

来看下面的代码

1//db.go
2package db
3type Store struct {
4   db *sql.DB
5}
6func NewDB() *Store { ... } //func to initialise DB
7func (s *Store) Insert(item interface{}) error { ... } //insert item
8func (s *Store) Get(id int) error { ... } //get item by id

这里db.go简单提供了一些插入和读取的方法

1//user.go
2package user
3type UserStore interface {
4   Insert(item interface{}) error
5   Get(id int) error
6}
7type UserService struct {
8   store UserStore
9}
10// Accepting interface here!
11func NewUserService(s UserStore) *UserService {
12   return &UserService{
13      store: s,
14   }
15}
16func (u *UserService) CreateUser() { ... }
17func (u *UserService) RetrieveUser(id int) User { ... }

消费者user.go需要数据存储的相关依赖才能执行与用户相关的业务逻辑,它不关心存储具体是怎么实现的,而只需要关心它具体需要什么

所以,它只关心它需要2个方法:Insert()Get(),因此,它能够实现创建和查询用户

因此,它定义了自己的接口UserStore并接收它作为其依赖项,而db.go中的Store结构实现了该接口,所以其可作依赖项

所以说,接受接口就是让消费者在接口中定义他们想要的内容,消费者不用担心谁能实现(我们帮他实现),只需要关注这个接口可以执行消费者需要的任务即可

而这样做,也会带来一些好处:

  • 更松散的耦合,以及更灵活

通过接受接口,消费者不会与其依赖关系耦合。如果明天我决定使用 MySQL 而不是 Postgres,则user.go根本不需要更改。只要满足消费者定义的接口,这就保留了使用任何存储的灵活性。

  • 更容易测试
  • 测试也会变得更简单,因为我们可以轻松地传递内存中的模拟,而不必启动实际的数据库实例,这对于单元测试来说可能会很麻烦。我们可以拥有一个模拟内存存储,其中包含测试用例所需的适当数据。
1type inMemStore struct {
2    mp map[string]interface{}
3}
4
5//user_test.gofunc TestCreateUser(t *testing.T) {
6   s := new(inMemStore) //use some in-memory store...
7   service := NewUserService(s)
8   
9   //... test the CreateUser() function
10}

Producers return concrete types(生产者返回具体的类型)

也就是说,producer应该向consumer提供具体的类型而不是接口

为什么呢?

producer不一定只为某个consumer提供服务,如果很多consumer都需要用到producer中的方法(以接口的形式),那producer就得为每个consumer都返回某个特定的接口,但这就违背了我们返回接口的目的

上面的示例中,NewDB()返回具体类型给消费者,而消费者可以隐式地将

*Store转化为UserService,其他消费者也同理,这样就可以利用这个机制,免去了为了适配各个接口的New过程

我们这里,也会给出这个Bad Case

1//postgres.go
2package db
3
4type Store interface {
5   Insert(item interface{}) error
6   Get(id int) error
7}
8
9type MyStore struct {
10   db *sql.DB
11}
12
13func InitDB() Store { ... } //func to initialise DB
14func (s *Store) Insert(item interface{}) error { ... } //insert item
15func (s *Store) Get(id int) error { ... } //get item by id
16
17
18
19//user.go
20package user
21
22type UserService struct {
23   store db.Store
24}
25
26func NewUserService(s db.Store) *UserService {
27   return &UserService{
28      store: s,
29   }
30}
31func (u *UserService) CreateUser() { ... }
32func (u *UserService) RetrieveUser(id int) User { ... }

该接口现在由生产者定义,消费者使用该接口作为入口点。这被称为抢占式接口,即生产者在实际使用接口之前抢先定义接口。

有些人可能会认为,让生产者返回一个接口,可以让开发人员专注于函数发出的 API。然而,这在 Go 中是不必要的;隐式接口允许在事后进行优雅的抽象,而不需要你预先进行抽象。

Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.(在使用接口之前不要定义它们:如果没有实际的使用示例,很难看出接口是否必要,更不用说它应该包含哪些方法了)

如果没有实际的使用示例,就很难看出接口是否必要,更不用说它应该包含哪些方法了。

这里涉及到了,应该先编写接口还是先编写实例的问题

  • 假设先编写实例,这当然可以,你可以把它可能有的方法全部列举出来,比如对于一个数据库实例,如上面的Store类型,它肯定是有CRUD这4个方法的,但是有些情况,我们却不需要U(update),那你写的方法就没用了
  • 假设先编写接口,像上面的user那样,它清楚自己需要什么,但这对我们编写代码有什么帮助呢?很简单,借助interface,我们在写它的相关服务的时候,就很清楚如何去编写,不需要考虑谁来实现(因为我们后面肯定会实现的),这样,我们就可以很轻松地完成这个服务的逻辑(因为我们有任何我们需要的方法,没有可以自己添加嘛),后续我们只需要专注于如何实现那些接口就可以了

上面实际上是两种方向,自下而上自上而下

自下而上固然可行,但是它缺乏了对全局的把控,因为你的高度低了,你只能把控你自己以及比你低一层级的,这会让你前期虽然写的很爽,但后期会花费大量的时间修改代码

而自上而下,拥有较为广阔的视野,因为我们,清楚自己需要哪些方法,由此,来决定下层的走向,这种编写方式,理论上可以,越写越爽,越爽越写,从此在coding的路上一去不复返😜

参考文献:https://go.dev/wiki/CodeReviewComments#interfaces

https://sanyuesha.com/2017/07/22/how-to-understand-go-interface/