Golang 接口 interface

1. Golang 接口

1.1. 接口实现

一个接口类型定义了一套方法, 如果一个具体类型要实现该接口, 那么必须实现接口类型定义中的所有方法。有同学可能觉得, 这个概念很简单啊, 先看个示例:

type tester interface {
	test()
	string() string
}

type data struct{}

func (*data) test() {}

func (data) string() string {
	return ""
}

func main() {
	var d data
	// var t tester = d  // 错误: test() 不属于 data 的方法集, 所以不支持转换
	var t tester = &d // 结构类型是可以直接转换为接口类型
	t.test()
	println(t.string())
}

编译器是根据方法集来判断是否实现了接口, 显然再上例中只有 *data 才符合 tester 的要求, 所以不支持 var t tester = d 的转换, 但支持 var t tester = &d。可能有同学会觉得这个也和容易, 那么当和"嵌入字段"结合起来, 这里的转换逻辑可能就比较复杂了, 因为只有相同的"方法集"(我再强调一下, 方法集必须是包含或者相等关系, 比如 A 必须包含 B 所有的方法集, A 才能赋值给 B), 才能进行换行, 具体的包含关系规则, 详见上一章的"方法集"。

1.2. 接口组合

接口类型间的嵌入也被称为接口的组合, 这个我们很容易联想到结构体类型的嵌入字段, 但是两者有很大不同, 接口类型间的嵌入要更简单一些, 因为它不会涉及方法间的"屏蔽", 只要组合的接口之间有同名的方法就会产生冲突, 从而无法通过编译。所以接口体类型组合会存在方法"屏蔽"现象, 但是接口不会, 这个是两者非常重要的区别! 下面我们看一个接口组合的示例:

type stringer interface {
	string() string
}

type tester interface {
	stringer // 嵌入 stringer 接口
	tester()
}

type data struct{}

func (*data) test() {}

func (data) string() string {
	return ""
}

func pp(a stringer) { // 超级接口变量, 可以隐式转换为子集, 反过来不行
	println(a.string())
}

func main() {
	var d data
	var t tester = &d  // *data 包含 tester 所有的方法集, 实现了 tester 接口
	pp(t)              // 隐式转换为接口子集 stringer
	var s stringer = t // 显示转换为接口子集 stringer
	println(s.string())
	// var t2 tester = s  // 接口不能逆向转换
}

通过上面的示例, 我们可以得出一个结论: 接口变量可显式/隐式转换为子集, 但是不能逆向转换。然后上面的转换过程中, 还是绕不开"方法集"的概念, 比如写成 var t tester = d 就不行了, 这个概念我再给大家强化一下。

1.3. 动态类型

对于一个接口类型的变量来说, 我们赋给它的值可以被叫做它的实际值(也称动态值), 而该值的类型可以被叫做这个变量的实际类型(也称动态类型)。也就是说, 一个接口类型的值(简称为接口值)其实有两个部分: 动态类型 + 动态值, 下面看一个示例:

type Pet interface {
	Name() string
}

type Dog struct {
}

type Cat struct {
}

func (d Dog) Name() string {
	return "Dog"
}

func (d Dog) Language() string {
	return "汪汪汪"
}

func (d Cat) Name() string {
	return "Cat"
}

func (d Cat) Color() string {
	return "Black"
}

func main() {
	var p pet
	var dog1 Dog
	var cat1 Cat
	pet = dog1          // 动态类型为 Dog
	println(pet.Name()) // 输出: Dog, 其实调用的是 dog.Name()
	pet = cat1          // 动态类型为 Cat
	println(pet.Name()) // 输出: Cat, 其实调用的是 cat.Name()
	pet = nil           // 动态类型和值都是 nil
}

动态类型这个叫法是相对于静态类型而言的, 对于变量 pet 来讲, 它的静态类型就是 Pet, 并且永远是 Pet, 但是它的动态类型却会随着我们赋给它的动态值而变化。所以执行 pet = dog1 时, pet 的动态类型为 Dog, 动态值是 dog1 的副本; 执行 pet = cat1 时, pet 的动态类型为 Cat, 动态值是 cat1 的副本。我们可以通过 Go 语言的这个特性, 来实现 C++ 中多态的方法特性, 详见下一个小节。

1.4. 内嵌接口

我们可以通过结构体内嵌结构体, 实现"匿名字段"和"方法覆盖"。也可以通过接口内嵌接口, 也就是 6.2 中的"接口组合"。但是对于结构体内嵌结构的使用, 这个可能遇到的比较少:

type Interface interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

type reverse struct{ Interface }

大家可能对这用用法比较疑惑, 不知道这种方法具体的使用场景是什么? 下面我们来看一个完整的例子, 以下代码是从 sort 包提取出来的:

type Interface interface {
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

// Array 实现 Interface 接口
type Array []int

func (arr Array) Len() int {
	return len(arr)
}

func (arr Array) Less(i, j int) bool {
	return arr[i] < arr[j]
}

func (arr Array) Swap(i, j int) {
	arr[i], arr[j] = arr[j], arr[i]
} // 匿名接口 (anonymous interface)

type reverse struct{ Interface } // 重写 (override)

func (r reverse) Less(i, j int) bool {
	return r.Interface.Less(j, i)
}

// 构造 reverse Interface
func Reverse(data Interface) Interface {
	return &reverse{data}
}

func main() {
	arr := Array{1, 2, 3}
	rarr := Reverse(arr)
	fmt.Println(arr.Less(0, 1))
	fmt.Println(rarr.Less(0, 1))
}

sort 包中这么写的目的是为了重写 Interface 的 Less 方法, 并有效利用了原始的 Less 方法; 通过 Reverse 可以从 Interface 构造出一个反向的 Interface。go 语言利用组合的特性, 寥寥几行代码就实现了重写。对比一下传统的组合匿名结构体实现重写的写法, 或许可以更好的帮助我们了解匿名接口的优点:

// 同上, 全部省略。// 匿名 struct
type reverse struct{ Array } // 重写

func (r reverse) Less(i, j int) bool {
	return r.Array.Less(j, i)
}

// 构造 reverse Interface
func Reverse(data Array) Interface {
	return &reverse{data}
}

func main() {
	arr := Array{1, 2, 3}
	rarr := Reverse(arr)
	fmt.Println(arr.Less(0, 1))
	fmt.Println(rarr.Less(0, 1))
}

上面这个例子使用了匿名结构体的写法, 和之前匿名接口的写法实现了同样的重写功能, 甚至非常相似。但是仔细对比一下你就会发现匿名接口的优点, 匿名接口的方式不依赖具体实现, 可以对任意实现了该接口的类型进行重写。这在写一些公共库时会非常有用, 如果你经常看一些库的源码, 匿名接口的写法应该会很眼熟。这里我总结结构体内嵌接口的作用:

  • 不依赖具体实现: 即接口为 A, 结构体 B1、B2 实现了接口 A, 结构体 C 内嵌了 A, 那么 C.A 可以通过 B1/B2 实例化;
  • 对接口类型进行重写: 当 C.A 通过 B1 实例化后, C 和 B1 的关系, 可以转变为接口体 C 内嵌结构体 B1, 那么 C 可以直接使用 B1 中的所有方法, 当然 C 也可以对 B1 中的方法进行重写, 这里官方文档这样解释"Interface and we can override a specific method without having to define all the others."

1.5. 实现多态的方法

我们先看一个示例:

type IMessage interface {
	Print()
}
type BaseMessage struct {
	//IMessage 没有必要 embedding 这个 interface, 因为只是按照契约实现接口, 但是并没有利用接口的数据和功能
	//一般说来直接实现接口的类都没有必要 embedding 接口
	msg string
}

func (message *BaseMessage) Print() {
	fmt.Println("baseMessage:", message.msg)
}

type SubMessage struct {
	BaseMessage //因为要使用 BaseMessage 的数据, 所以必须 embedding
}

func (message *SubMessage) Print() {
	fmt.Println("subMessage:", message.msg)
}

func interface_use(i IMessage) {
	i.Print()
}

func main() {
	baseMessage := new(BaseMessage)
	baseMessage.msg = "a"
	interface_use(baseMessage) // 输出: baseMessage:a
	SubMessage := new(SubMessage)
	SubMessage.msg = "b"
	interface_use(SubMessage) // 输出: subMessage:b
}

对于上面代码, 看起来可能不难, 但是用到的知识点却非常多, 我们解读一下:

  • 实现接口: 结构体 BaseMessage, 实现了 IMessage 接口的所有方法;
  • 匿名字段嵌入 + 方法覆盖: 结构体 SubMessage 嵌入了 BaseMessage, 也实现了自己的 Print(), 所以实际使用时, SubMessage.Print() 会覆盖 BaseMessage.Print();
  • 接口转换: BaseMessageSubMessage 可以转换为 IMessage 接口, 代码里面是隐式转换;
  • 动态类型: BaseMessageSubMessage 都实现了 IMessage 接口, 所以通过隐式转换后, IMessage 接口的动态类型和动态值会相应改变, 当动态类型为 BaseMessage 时, 执行 i.Print(), 执行的是 BaseMessage.Print(); 当动态类型为 SubMessage, 执行 i.Print(), 执行的是 SubMessage.Print()

1.6. 实战: 设计模式之组合模式

相关内容可以参考: http://tigerb.cn/go-patterns/#/?id=%e7%bb%84%e5%90%88%e6%a8%a1%e5%bc%8f, 里面的代码精简如下:

// Context 上下文 type Context struct{}
// Component 组件接口 type Component interface {    Mount(c Component, components ...Component) error  // 添加一个子组件    Remove(c Component) error  // 移除一个子组件    Do(ctx *Context) error  // 执行组件&子组件}// BaseComponent 基础组件// 实现 Add: 添加一个子组件// 实现 Remove: 移除一个子组件 type BaseComponent struct {    ChildComponents []Component // 子组件列表}// Mount 挂载一个子组件 func (bc *BaseComponent) Mount(c Component, components ...Component) (err error) {    bc.ChildComponents = append(bc.ChildComponents, c)    if len(components) == 0 {        return    }    bc.ChildComponents = append(bc.ChildComponents, components...)    return}// Remove 移除一个子组件 func (bc *BaseComponent) Remove(c Component) (err error) {    if len(bc.ChildComponents) == 0 {        return    }    for k, childComponent := range bc.ChildComponents {        if c == childComponent {            fmt.Println(runFuncName(), "移除: ", reflect.TypeOf(childComponent))            bc.ChildComponents = append(bc.ChildComponents[:k], bc.ChildComponents[k+1:]...)        }    }    return}// Do 执行组件&子组件 func (bc *BaseComponent) Do(ctx *Context) (err error) {    // do nothing    return}// ChildsDo 执行子组件 func (bc *BaseComponent) ChildsDo(ctx *Context) (err error) {    // 执行子组件    for _, childComponent := range bc.ChildComponents {        if err = childComponent.Do(ctx); err != nil {            return err        }    }    return}
// CheckoutPageComponent 订单结算页面组件 type CheckoutPageComponent struct {    // 合成复用基础组件    BaseComponent}// Do 执行组件&子组件 func (bc *CheckoutPageComponent) Do(ctx *Context) (err error) {    // 当前组件的业务逻辑写这    fmt.Println(runFuncName(), "订单结算页面组件。..")    // 执行子组件    bc.ChildsDo(ctx)    // 当前组件的业务逻辑写这    return}// AddressComponent 地址组件 type AddressComponent struct {    BaseComponent}func (bc *AddressComponent) Do(ctx *Context) (err error) {    fmt.Println(runFuncName(), "地址组件。..")    bc.ChildsDo(ctx)    return}// StoreComponent 店铺组件 type StoreComponent struct {    BaseComponent}func (bc *StoreComponent) Do(ctx *Context) (err error) {    fmt.Println(runFuncName(), "店铺组件。..")    bc.ChildsDo(ctx)    return}// SkuComponent 商品组件 type SkuComponent struct {    BaseComponent}func (bc *SkuComponent) Do(ctx *Context) (err error) {    fmt.Println(runFuncName(), "商品组件。..")    bc.ChildsDo(ctx)    return}// PromotionComponent 优惠信息组件 type PromotionComponent struct {    BaseComponent}func (bc *PromotionComponent) Do(ctx *Context) (err error) {    fmt.Println(runFuncName(), "优惠信息组件。..")    bc.ChildsDo(ctx)    return}// ExpressComponent 物流组件 type ExpressComponent struct {    BaseComponent}func (bc *ExpressComponent) Do(ctx *Context) (err error) {    fmt.Println(runFuncName(), "物流组件。..")    bc.ChildsDo(ctx)    return}// AftersaleComponent 售后组件 type AftersaleComponent struct {    BaseComponent}func (bc *AftersaleComponent) Do(ctx *Context) (err error) {    fmt.Println(runFuncName(), "售后组件。..")    bc.ChildsDo(ctx)    return}func main() {    // 初始化订单结算页面 这个大组件    checkoutPage := &CheckoutPageComponent{}    // 挂载子组件    storeComponent := &StoreComponent{}    skuComponent := &SkuComponent{}    skuComponent.Mount(        &PromotionComponent{},        &AftersaleComponent{},    )    storeComponent.Mount(        skuComponent,        &ExpressComponent{},    )    // 挂载组件    checkoutPage.Mount(        &AddressComponent{},        storeComponent,    )    // 开始构建页面组件数据    checkoutPage.Do(&Context{})}

这里其实不是为了去讲设计模式, 主要是希望能借鉴这个示例, 来巩固上面的知识, 里面的代码细节, 我就不再给大家剖析了, 等后面有时间, 我再把这个示例的前因后果, 整体再讲一下。

1.7. 实战: 设计模式之工厂 + 策略模式

type Pool interface {   Get() (io.Closer, error)   Put(obj io.Closer)   Close() error}// 工厂方法 func NewPool(type, name string, size int, newFunc func() (io.Closer, error)) Pool {   if type == "chanPool" {       return NewChanPool(name, size, newFunc)   } else {       return NewRingBufferPool(name, size, newFunc)   }}
// type chanPool struct {   name string   size int   idle int32   max  int32   ch   chan io.Closer   new  func() (io.Closer, error)}func NewChanPool(name string, size int, newFunc func() (io.Closer, error)) Pool {   return &chanPool{      name: name,      size: size,      ch:   make(chan io.Closer, size),      new:  newFunc,   }}// 假如实现了 Pool 接口, 实现方法省略。
type ringBufferPool struct {   sync.Once   err    error   closed int32   name   string   rb     *RingBuffer   new    func() (io.Closer, error)}func NewRingBufferPool(name string, size int, newFunc func() (io.Closer, error)) Pool {   return &ringBufferPool{      err:  errors.New("failed get object from ring buffer pool " + name),      name: name,      rb:   NewRingBuffer(int32(size)),      new:  newFunc,   }}// 实现了 Pool 接口, 实现方法省略。
func main() {    var pool    pool := NewPool("chanPool", "testpool", 10, nil)    pool.Close() // 调用的是 chanPool 的 Close 方法
    pool := NewPool("ringBufferPool", "testpool", 10, nil)    pool.Close() // 调用的是 ringBufferPool 的 Close 方法}

这个示例, 就当给大家额外学习, 本来是不打算写在这里面的。实例化了 2 个结构体, 分别为 chanPool 和 ringBufferPoo, 隐式转换为接口 pool 后, 可以通过 pool 中的方法, 动态调用实例化对象的方法。

1.8. 总结

接口这块知识, 其实在我没有完全梳理完之前, 总感觉有些知识点比较模糊, 当碰到比较复杂的代码, 就有种没有摸透的感觉, 现在将它们全部梳理完后, 感觉里面的语法就清晰了很多。对于接口实现, 如果大家对方法集不是特别清楚, 这里其实是很容易入坑的, 因为是否实现了该接口, 编译器是根据方法集来判断的。然后就是接口组合, 之前知道接口体有匿名字段, 可以进行组合, 这里又搞个接口组合, 就有点迷糊了, 其实两者有异曲同工, 重要的区分就是接口组合是没有屏蔽的概念, 如果定义相同的接口方法, 编译器会直接报错的。对于动态类型, 这个可能是接口中稍微比较复杂的地方, 我们要弄清楚的是, 接口变量的动态值、动态类型都代表了什么, 这些其实都是正确使用接口变量的基础。最后通过"实现多态的方法"和"设计模式之组合模式"两小节, 将结构体、方法、匿名字段、动态类型、方法屏蔽、隐式转换等知识全部串起来, 这些知识虽然看起来比较零散(很多书籍讲述这些知识时, 也都是零散的去讲, 有的甚至完全没有去讲, 所以一直不能将这些知识点系统的串起来, 这个也是我想写这个手册的初衷之一), 但是他们直接其实有着千世万缕的联系。最后, 对于结构体内嵌结构体, 结构体内嵌接口, 然后一些常用专有名词的理解和设计模式的运用, 我画个大图, 大家可以一目了然。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值