文章目录

  • Golang interface 接口详细原理和使用技巧
    • 一、Go interface 介绍
      • interface 在 Go 中的重要性说明
      • interface 的特性
      • interface 接口和 reflect 反射
    • 二、Go 里面为啥偏向使用 Interface
      • 可以实现泛型编程(虽然 Go 在 1.18 之后已经支持泛型了)
      • 可以隐藏具体的实现
      • 可以实现面向对象编程中的多态用法
      • 空接口可以接受任何类型的参数
    • 三、Go interface 的常见应用和实战技巧
      • interface 接口赋值
        • 通过对象实例赋值
        • 通过接口赋值
      • interface 接口嵌套
      • interface 强制类型转换
        • ret, ok := interface.(type) 断言
        • switch x.(type) 断言
      • interface 与 nil 的比较
    • 最后

Golang interface 接口详细原理和使用技巧

  • 首次记录 2017 年下半年
  • 再次更新 2022 年底

一、Go interface 介绍

interface 在 Go 中的重要性说明

interface 接口在 Go 语言里面的地位非常重要,是一个非常重要的数据结构,只要是实际业务编程,并且想要写出优雅的代码,那么必然要用上 interface,因此 interface 在 Go 语言里面处于非常核心的地位。

我们都知道,Go 语言和典型的面向对象的语言不太一样,Go 在语法上是不支持面向对象的类、继承等相关概念的。但是,并不代表 Go 里面不能实现面向对象的一些行为比如继承、多态,在 Go 里面,通过 interface 完全可以实现诸如 C++ 里面的继承 和 多态的语法效果。

interface 的特性

Go 中的 interface 接口有如下特性:

  • 关于接口的定义和签名

    • 接口是一个或多个方法签名的集合,接口只有方法声明,没有实现,没有数据字段,只要某个类型拥有该接口的所有方法签名,那么就相当于实现了该接口,无需显示声明了哪个接口,这称为 Structural Typing。
    • interface 接口可以匿名嵌入其他接口中,或嵌入到 struct 结构中
    • 接口可以支持匿名字段方法
  • 关于接口赋值

    • 只有当接口存储的类型和对象都为 nil 时,接口才等于 nil
    • 一个空的接口可以作为任何类型数据的容器
    • 如果两个接口都拥有相同的方法,那么它们就是等同的,任何实现了他们这个接口的对象之间,都可以相互赋值
    • 如果某个 struct 对象实现了某个接口的所有方法,那么可以直接将这个 struct 的实例对象直接赋值给这个接口类型的变量。
  • 关于接口嵌套,Go 里面支持接口嵌套,但是不支持递归嵌套

  • 通过接口可以实现面向对象编程中的多态的效果

interface 接口和 reflect 反射

在 Go 的实现里面,每个 interface 接口变量都有一个对应 pair,这个 pair 中记录了接口的实际变量的类型和值(value, type),其中,value 是实际变量值,type 是实际变量的类型。任何一个 interface{} 类型的变量都包含了2个指针,一个指针指向值的类型,对应 pair 中的 type,这个 type 类型包括静态的类型 (static type,比如 int、string…)和具体的类型(concrete type,interface 所指向的具体类型),另外一个指针指向实际的值,对应 pair 中的 value。

interface 及其 pair 的存在,是 Go 语言中实现 reflect 反射的前提,理解了 pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair 对的一种机制。

二、Go 里面为啥偏向使用 Interface

Go 里面为啥偏向使用 Interface 呢? 主要原因有如下几点:

可以实现泛型编程(虽然 Go 在 1.18 之后已经支持泛型了)

在 C++ 等高级语言中使用泛型编程非常的简单,但是 Go 在 1.18 版本之前,是不支持泛型的,而通过 Go 的接口,可以实现类似的泛型编程,如下是一个参考示例

    package sort// A type, typically a collection, that satisfies sort.Interface can be// sorted by the routines in this package.  The methods require that the// elements of the collection be enumerated by an integer index.type Interface interface {// Len is the number of elements in the collection.Len() int// Less reports whether the element with// index i should sort before the element with index j.Less(i, j int) bool// Swap swaps the elements with indexes i and j.Swap(i, j int)}...// Sort sorts data.// It makes one call to data.Len to determine n, and O(n*log(n)) calls to// data.Less and data.Swap. The sort is not guaranteed to be stable.func Sort(data Interface) {// Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.n := data.Len()maxDepth := 0for i := n; i > 0; i >>= 1 {maxDepth++}maxDepth *= 2quickSort(data, 0, n, maxDepth)}

Sort 函数的形参是一个 interface,包含了三个方法:Len(),Less(i,j int),Swap(i, j int)。使用的时候不管数组的元素类型是什么类型(int, float, string…),只要我们实现了这三个方法就可以使用 Sort 函数,这样就实现了“泛型编程”。

这种方式,我在闪聊项目里面也有实际应用过,具体案例就是对消息排序。 下面给一个具体示例,代码能够说明一切,一看就懂:

    type Person struct {Name stringAge  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)}

可以隐藏具体的实现

隐藏具体的实现,是说我们提供给外部的一个方法(函数),但是我们是通过 interface 接口的方式提供的,对调用方来说,只能通过 interface 里面的方法来做一些操作,但是内部的具体实现是完全不知道的。

例如我们常用的 context 包,就是这样设计的,如果熟悉 Context 具体实现的就会很容易理解。详细代码如下:

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }}

可以看到 WithCancel 函数返回的还是一个 Context interface,但是这个 interface 的具体实现是 cancelCtx struct。

        // 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.Mutexchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel call}func (c *cancelCtx) Done() <-chan struct{} {return c.done}func (c *cancelCtx) Err() error {c.mu.Lock()defer c.mu.Unlock()return c.err}func (c *cancelCtx) String() string {return fmt.Sprintf("%v.WithCancel", c.Context)}

尽管内部实现上下面三个函数返回的具体 struct (都实现了 Context interface)不同,但是对于使用者来说是完全无感知的。

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)    //返回 cancelCtxfunc WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtxfunc WithValue(parent Context, key, val interface{}) Context    //返回 valueCtx

可以实现面向对象编程中的多态用法

interface 只是定义一个或一组方法函数,但是这些方法只有函数签名,没有具体的实现,这个 C++ 中的虚函数非常类似。在 Go 里面,如果某个数据类型实现 interface 中定义的那些函数,则称这些数据类型实现(implement)了这个接口 interface,这是我们常用的 OO 方式,如下是一个简单的示例

    // 定义一个 SimpleLog 接口type SimpleLog interface {Print()}func TestFunc(x SimpleLog) {}// 定义一个 PrintImpl 结构,用来实现 SimpleLog 接口type PrintImpl struct {}// PrintImpl 对象实现了SimpleLog 接口的所有方法(本例中是 Print 方法),就说明实现了  SimpleLog 接口func (p *PrintImpl) Print() {}func main() {var p PrintImplTestFunc(p)}

空接口可以接受任何类型的参数

空接口比较特殊,它不包含任何方法:interface{} ,在 Go 语言中,所有其它数据类型都实现了空接口,如下:

var v1 interface{} = 1
var v2 interface{} = "abc"
var v3 interface{} = struct{ X int }{1}

因此,当我们给 func 定义了一个 interface{} 类型的参数(也就是一个空接口)之后,那么这个参数可以接受任何类型,官方包中最典型的例子就是标准库 fmt 包中的 Print 和 Fprint 系列的函数。

一个简单的定义示例方法如下:

 Persist(context context.Context, msg interface{}) bool

msg 可以为任何类型,如 pb.MsgInfo or pb.GroupMsgInfo,定义方法的时候可以统一命名模块,实现的时候,根据不同场景实现不同方法。

三、Go interface 的常见应用和实战技巧

interface 接口赋值

可以将一个实现接口的对象实例赋值给接口,也可以将另外一个接口赋值给接口。

通过对象实例赋值

将一个对象实例赋值给一个接口之前,要保证该对象实现了接口的所有方法。在 Go 语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口,这个是非侵入式接口的设计模式,非侵入式接口一个很重要的优势就是可以免去面向对象里面那套比较复杂的类的继承体系。

在 Go 里面,面向对象的那套类的继承体系就不需要关心了,定义接口的时候,我们只需关心这个接口应该提供哪些方法,当然,按照 Go 的原则,接口的功能要尽可能的保证职责单一。而对应接口的实现,也就是接口的调用方,我们只需要知道这个接口定义了哪些方法,然后我们实现这些方法就可以了,这个也无需提前规划,调用方也无需关系是否有其他模块定义过类似的接口或者实现,只关注自身就行。

考虑如下示例:

type Integer int
func (a Integer) Less(b Integer) bool {return a < b
}
func (a *Integer) Add(b Integer) {*a += b
}
type LessAdder interface { Less(b Integer) bool Add(b Integer)
}
var a Integer = 1
var b1 LessAdder = &a  //OK
var b2 LessAdder = a   //not OK

b2 的赋值会报编译错误,为什么呢?因为这个:The method set of any other named type T consists of all methods with receiver type T. The method set of the corresponding pointer type T is the set of all methods with receiver T or T (that is, it also contains the method set of T).也就是说 *Integer 这个指针类型实现了接口 LessAdder 的所有方法,而 Integer 只实现了 Less 方法,所以不能赋值。

通过接口赋值

        var r io.Reader = new(os.File)var rw io.ReadWriter = r   //not okvar rw2 io.ReadWriter = new(os.File)var r2 io.Reader = rw2    //ok

因为 r 没有 Write 方法,所以不能赋值给rw。

interface 接口嵌套

io package 中的一个接口:

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {ReaderWriter
}

ReadWriter 接口嵌套了 io.Reader 和 io.Writer 两个接口,实际上,它等同于下面的写法:

type ReadWriter interface {Read(p []byte) (n int, err error) Write(p []byte) (n int, err error)
}

注意,Go 语言中的接口不能递归嵌套,如下:

// illegal: Bad cannot embed itself
type Bad interface {Bad
}
// illegal: Bad1 cannot embed itself using Bad2
type Bad1 interface {Bad2
}
type Bad2 interface {Bad1
}

interface 强制类型转换

ret, ok := interface.(type) 断言

在 Go 语言中,可以通过 interface.(type) 的方式来对一个 interface 进行强制类型转换,但是如果这个 interface 被转换为一个不包含指定类型的类型,那么就会出现 panic 。因此,实战应用中,我们通常都是通过 ret, ok := interface.(type)这种断言的方式来优雅的进行转换,这个方法中第一个返回值是对应类型的值,第二个返回值是类型是否正确,只有 ok = true 的情况下,才说明转换成功,最重要的是,通过这样的转换方式可以避免直接转换如果类型不对的情况下产生 panic。

如下是一个以 string 为类型的示例:

str, ok := value.(string)
if ok {fmt.Printf("string value is: %q\n", str)
} else {fmt.Printf("value is not a string\n")
}如果类型断言失败,则str将依然存在,并且类型为字符串,不过其为零值,即一个空字符串。

switch x.(type) 断言

查询接口类型的方式为:

switch x.(type) {// cases :
}

示例如下:

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:return str //type of str is string
case int: return int //type of str is int
}
语句switch中的value必须是接口类型,变量str的类型为转换后的类型。

interface 与 nil 的比较

interface 与 nil 的比较是挺有意思的,例子是最好的说明,如下例子:

package mainimport ("fmt""reflect"
)type State struct{}func testnil1(a, b interface{}) bool {return a == b
}func testnil2(a *State, b interface{}) bool {return a == b
}func testnil3(a interface{}) bool {return a == nil
}func testnil4(a *State) bool {return a == nil
}func testnil5(a interface{}) bool {v := reflect.ValueOf(a)return !v.IsValid() || v.IsNil()
}func main() {var a *Statefmt.Println(testnil1(a, nil))fmt.Println(testnil2(a, nil))fmt.Println(testnil3(a))fmt.Println(testnil4(a))fmt.Println(testnil5(a))
}

运行后返回的结果如下

false
false
false
true
true

为什么是这个结果?

*因为一个 interface{} 类型的变量包含了2个指针,一个指针指向值的类型,另外一个指针指向实际的值。对一个 interface{} 类型的 nil 变量来说,它的两个指针都是0;但是 var a State 传进去后,指向的类型的指针不为0了,因为有类型了, 所以比较为 false。 interface 类型比较, 要是两个指针都相等,才能相等。

最后

请允许我打个小广告:这篇文章首发在我微信公众号【后端系统和架构】中,点击这里可以去往公众号查看原文链接,如果对你有帮助,欢迎前往关注,更加方便快捷的接收最新优质文章

Golang interface 接口详细原理和使用技巧相关推荐

  1. Golang Context 详细原理和使用技巧

    文章目录 Golang Context 详细原理和使用技巧 Context 背景 和 适用场景 Context 的背景 Context 的功能和目的 Context 的基本使用 Context 的同步 ...

  2. GoLang之接口转换的原理(8)

    文章目录 GoLang之接口转换的原理(8) GoLang之接口转换的原理(8) 通过前面提到的 iface 的源码可以看到,实际上它包含接口的类型 interfacetype 和 实体类型的类型 _ ...

  3. Go的50坑:新Golang开发者要注意的陷阱、技巧和常见错误[1]

    Go的50坑:新Golang开发者要注意的陷阱.技巧和常见错误[1] 初级篇 开大括号不能放在单独的一行 未使用的变量 未使用的Imports 简式的变量声明仅可以在函数内部使用 使用简式声明重复声明 ...

  4. Golang之接口底层分析

    目录 GoLang之iface 和 eface 的区别是什么? GoLang之接口的动态类型和动态值 [引申1]接口类型和 `nil` 作比较 [引申2] [引申3]如何打印出接口的动态类型和值? G ...

  5. Golang 侧数据库连接池原理和参数调优

    Golang 侧数据库连接池原理和参数调优 文章目录 Golang 侧数据库连接池原理和参数调优 数据库连接池 数据库连接池的设计 Go 的数据库连接池 Go 数据库连接池的设计 建立连接 释放连接 ...

  6. Linux 企业级安全原理和防范技巧

    Linux 企业级安全原理和防范技巧 1. 企业级Linux系统防护概述 1.1 企业级Linux系统安全威胁 1.2 企业级Linux系统安全立体式防范体系 1.2.1 Linux文件系统访问安全 ...

  7. java枚举类型原理_Java枚举类接口实例原理解析

    这篇文章主要介绍了Java枚举类接口实例原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 枚举类可以实现一个或多个接口.与普通类实现接口完全一 ...

  8. 步进电机28BYJ-48的驱动(arduino,STM32平台),最全的驱动详细原理,驱动电路分析,驱动代码解释

    步进电机28BYJ-48的驱动(arduino平台,STM32),最全的驱动详细原理,驱动电路分析,驱动代码解释 目录 步进电机28BYJ-48的驱动(arduino平台,STM32),最全的驱动详细 ...

  9. Golang 中使用 JSON 的一些小技巧 陶文 陶文 3 个月前 有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用。 本来用一个json:,string 就可以支持了

    Golang 中使用 JSON 的一些小技巧 陶文 3 个月前 有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用. 本来用一个json:",string" ...

最新文章

  1. Redis 05_List列表 数组 Hash散列
  2. Java object方法与GC回收
  3. 一步步学习如何安装并使用SAP HANA Express Edition
  4. java解析html jsoup_2020-06-02 jsoup java解析html
  5. 赛锐信息:SAP进化论
  6. UIView转换为UIImage
  7. C语言读取图像像素坐标,OpenCL中读取image时的坐标
  8. 怎样import(导入)过期的磁带
  9. web 前端性能优化汇总
  10. 对convertView和ViewHolder的理解
  11. 美国大学计算机理论专业phd,揭秘美国大学计算机专业PHD申请难度
  12. 怎么建立局域网_tp-link无线路由器怎么设置 tp-link无线路由器设置方法【图文教程】...
  13. Git Bash复制粘贴快捷键失效问题
  14. 营销科学学会2021年年会揭晓玫琳凯博士论文奖得主
  15. 减半技术实现求a的n次幂
  16. 兵法三十六计第二计-围魏救赵。
  17. 百度easydl数据标注
  18. Java实现三角形图案绘制**
  19. 金岩石:陈晓失误已铸成大错
  20. 怎么获取机智股票自动交易软件

热门文章

  1. TreeView使用笔记
  2. 三星note5 android 7,国行三星note5安卓7.0降级6.0刷机包
  3. 2020最实用115个Java面试题及面向对象的能力汇总合集
  4. 市面上常见模拟器比对
  5. 计算机无法启动bios,开机进入bios无法进入系统怎么办_电脑开机就进入bios的解决方法...
  6. excel2016打开空白解决方法
  7. Java设计-微信小程序线上点餐/外卖项目+后台管理系统
  8. 视频剪辑工具,批量给视频添加上下图片
  9. RGB 颜色透明16进制表示
  10. pytorch 中的topk函数