如何用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/