golang-interface(一)为什么要使用interface

2,676 阅读7分钟

1. interface 是什么?有什么用?

1.1 日常生活中的 interface 举例

  • KFC的招牌就是一个接口,挂了KFC的招牌,我们不用进去就知道他家卖哪些食物,就可以直接去点上校鸡块、黄金烤鸡腿堡。没有挂这个招牌,就算卖的东西和KFC一模一样,我们不进去看菜单就不会知道。
  • 参考链接:www.zhihu.com/question/20…

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
}

// 可以理解cat类
type Cat struct{}
func (c Cat) Say() string { return "i am a cat" }
func (c Cat) Color() string {
	return "i am black"
}

// 可以理解为dog类
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)

	//car := Car{}
	//printSelf(car) 这两行不注释就会报错,因为car没有实现animal接口
}

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)
}

// ByAge implements sort.Interface for []Person based on
// the Age field.
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,但是具体实现却不一样,实现了功能的多样化
// 这里开始是 withCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{
        Context: parent,
        done:    make(chan struct{}),
    }
}

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
    Context     

    done chan struct{} // closed by the first cancel call.
    mu       sync.Mutex
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}


// 这里开始是 WithValue
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}
}

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
	Context
	key, val interface{}
}

3.3 示例二:设计角度-sql

  • 如下图,我们只需要输入sql,后面具体的实现是mysql,还是pg, 还是sqlite我们是不需要关心的

image.png

4. interface-解耦上下游的依赖

  • 上下游依赖没有那么重,维护起来就很好维护了

4.1 示例一:用户权限校验的实现

package main

// 假设这是redis客户端
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
	}
}
// 这里如果有其他的函数引用了 rds Redis, 那肯定也全部要改
// .......
func main() {
	token := "test"
	rds := Redis{} // 这里要修改
	AuthExpire(token, rds) // 这里rds这个名字可能要修改
}
  • 这是用接口实现检查一个用户的token
package main

type Cache interface {
	GetValue(key string) string
}

// 假设这是redis客户端
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{} //	Cache := MemoryCache{},修改这一句即可
	AuthExpire(token, cache)
}
  • 1,功能都可以实现,甚至不用接口的代码量更小
  • 2,如果我们的缓存(存储用户的组件)从redis换成了MemoryCache,
  • 3,首先我们都需要先编写MemoryCache实现原有redis的功能
  • 4,不用接口的时候,你的代码还需要更改的地方是所有用到了此缓存的函数。也就是说你需要去更改所有引用了Redis这个结构体的地方。示例中可能只是一个authExpire,但是实际上可能还有很多类似的函数引用了redis做缓存
  • 5,如果我们用了接口,那么我们需要修改的地方除开第三步都需要实现具体的功能,还有仅仅可能就是一个初始化的地方

5. interface-最最最常见的使用场景分析(重点重点)

  • 一般大家都看到过类似下面这样的代码,接下来我列出来代码中几个疑问点,并一一说出自己的理解
  • image.png
  • 我们为什么要提供New方法?
    • 因为我们的service struct 是小写开头,所以如果我们不用New方法的话,外部就没办法调用这个struct
  • 我们打算对外提供的 service struct那为什么要小写开头?
    • 因为如果我们大写开头,那么别人调用的时候,struct中可能部分元素不给赋值,则默认空值。这样会导致service的部分方法调用时会Panic。所以这里相当于是一个强制调用者必须按照我们的定义(也就是New方法)进行传参赋值
  • 那我们可以通过New方法对外进行返回service结构体从而提供服务,为什么要返回接口呢?
    • 第一个,用接口的话,使用者点进来第一时间就知道你提供了哪些方法(全部暴露在interface),简单明了,不用的话我还得一个个去找你对外提供了哪些服务,你这10个文件,我十个都需要看一下
    • 第二个,如果我们不使用接口,那么我们就和golang的基本理念相违背了 包中小写开头类型不对外进行暴露 原则,而使用接口则我们对外暴露接口即可
  • 里面有一行代码 var _ Service = (*service)(nil) 这句代码是用来做什么的
    • 这个其实在很多开源项目中会这样写,具体用处就是校验*service这个结构体,有没有实现Service这个接口
    • 可参考这个github issue:github.com/uber-go/gui…

6. 参考文章