1. interface 是什么?有什么用?
1.1 日常生活中的 interface 举例
1.2 golang 中interface的定义和用途
- golang 中 interface的定义
- interface 可以表示一种类型(任意一种类型)
- interface 是方法的集合(也就是接口的方法集合)
- 只要实现了接口中的所有方法,那么就认为你实现了这个接口
- interface的实际用途
- 一:多态的实现
- 二:隐藏函数的具体实现
- 三:中间层,解耦上下层依赖
- ps: 往后看从用途和示例去理解interface是什么,为什么要用interface
2. interface-实现多态
2.1 示例一:同理于编程最常见的鸭子说法
- 1,可以看到以下代码,实例化的Dog和Cat都可以传入introduceSelf,所以这里就实现了多态,因为传入的类型是不确定的
- 2,还有一点就是,新手可能会说这么麻烦,我直接实例化Dog后,调用对应要用的函数就好了。其实最开始写代码的时候我也是这么想的。所以我这里示例introduceSelf 是做了两件事,也就是说大家要考虑函数组合使用的,如果分别调用则会要做很多重复的操作,且不方便后期维护
package test
import (
"fmt"
"testing"
)
type animal interface {
Say() string
Color() string
}
type Cat struct{}
func (c Cat) Say() string { return "i am a cat" }
func (c Cat) Color() string {
return "i am black"
}
type Dog struct{}
func (d Dog) Say() string { return "i am a dog" }
func (d Dog) Color() string {
return "i am white"
}
type Car struct{}
func introduceSelf(input animal) {
fmt.Println(input.Say() + " and " + input.Color())
}
func TestMain1(t *testing.T) {
c := Cat{}
d := Dog{}
introduceSelf(c)
introduceSelf(d)
}
2.2 举例二:golang中的排序
- 1,这是一个排序的实现,通过实现Len,Swap,Less函数实现了sort的interface,从而调用sort.Sort然后实现排序(sort.Sort里面通过组合调用这三种方法进行了排序)
package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s: %d", p.Name, p.Age)
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func main() {
people := []Person{
{"Bob", 31},
{"John", 42},
{"Michael", 17},
{"Jenny", 26},
}
fmt.Println(people)
sort.Sort(ByAge(people))
fmt.Println(people)
}
3. interface-隐藏函数具体实现
3.1 隐藏函数具体实现有什么好处?
- 代码可阅读角度:简单易读,代码本来就是写给人看的,那自然是越容易懂越好
3.2 示例一:语言角度-golang中的context包
- withCancel 和 WithValue 返回的第一个参数都是context
- 但是各自返回的Context结构体又不是一样的
- withCancel : 返回结构体为 cancelCtx
- WithValue: 返回的结构体为 valueCtx
- 这样的话尽管返回的都是context,但是具体实现却不一样,实现了功能的多样化
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}
type cancelCtx struct {
Context
done chan struct{}
mu sync.Mutex
children map[canceler]struct{}
err error
}
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
type valueCtx struct {
Context
key, val interface{}
}
3.3 示例二:设计角度-sql
- 如下图,我们只需要输入sql,后面具体的实现是mysql,还是pg, 还是sqlite我们是不需要关心的
4. interface-解耦上下游的依赖
4.1 示例一:用户权限校验的实现
package main
type Redis struct {
}
func (r Redis) GetValue(key string) string {
panic("not implement")
}
func AuthExpire(token string, rds Redis) bool {
res := rds.GetValue(token)
if res == "" {
return false
} else {
return true
}
}
func main() {
token := "test"
rds := Redis{}
AuthExpire(token, rds)
}
package main
type Cache interface {
GetValue(key string) string
}
type Redis struct {
}
func (r Redis) GetValue(key string) string {
panic("not implement")
}
type MemoryCache struct {
}
func (m MemoryCache) GetValue(key string) string {
panic("not implement")
}
func AuthExpire(token string, cache Cache) bool {
res := cache.GetValue(token)
if res == "" {
return false
} else {
return true
}
}
func main() {
token := "test"
cache := Redis{}
AuthExpire(token, cache)
}
- 1,功能都可以实现,甚至不用接口的代码量更小
- 2,如果我们的缓存(存储用户的组件)从redis换成了MemoryCache,
- 3,首先我们都需要先编写MemoryCache实现原有redis的功能
- 4,不用接口的时候,你的代码还需要更改的地方是所有用到了此缓存的函数。也就是说你需要去更改所有引用了Redis这个结构体的地方。示例中可能只是一个authExpire,但是实际上可能还有很多类似的函数引用了redis做缓存
- 5,如果我们用了接口,那么我们需要修改的地方除开第三步都需要实现具体的功能,还有仅仅可能就是一个初始化的地方
5. interface-最最最常见的使用场景分析(重点重点)
- 一般大家都看到过类似下面这样的代码,接下来我列出来代码中几个疑问点,并一一说出自己的理解
- 我们为什么要提供New方法?
- 因为我们的service struct 是小写开头,所以如果我们不用New方法的话,外部就没办法调用这个struct
- 我们打算对外提供的 service struct那为什么要小写开头?
- 因为如果我们大写开头,那么别人调用的时候,struct中可能部分元素不给赋值,则默认空值。这样会导致service的部分方法调用时会Panic。所以这里相当于是一个强制调用者必须按照我们的定义(也就是New方法)进行传参赋值
- 那我们可以通过New方法对外进行返回service结构体从而提供服务,为什么要返回接口呢?
- 第一个,用接口的话,使用者点进来第一时间就知道你提供了哪些方法(全部暴露在interface),简单明了,不用的话我还得一个个去找你对外提供了哪些服务,你这10个文件,我十个都需要看一下
- 第二个,如果我们不使用接口,那么我们就和golang的基本理念相违背了 包中小写开头类型不对外进行暴露 原则,而使用接口则我们对外暴露接口即可
- 里面有一行代码 var _ Service = (*service)(nil) 这句代码是用来做什么的
6. 参考文章