← 返回文章

interface 用法介绍

简单介绍了interface的一些用法

参考链接:

  1. 深度解密Go语言之关于 interface 的 10 个问题
  2. golang: 类型转换和类型断言
  3. 接口 interface 详解
  4. Go编程模式:切片,接口,时间和性能

接口介绍

Golang 中的接口是一种抽象数据类型, Golang 中接口定义了对象的行为规范,只定义规范不实现。 接口中定义的规范由具体的对象来实现。

Golang interface 的定义

在 Golang 中 interface 是一种类型,一种抽象的类型。 interface 是一组函数 method 的集合,interface 中不能包含任何变量。

在 Golang 中 interface 中的所有方法都没有方法体,它定义了一个对象的行为规范,只定义规范不实现。 接口 体现了 程序设计的多态 和 高内聚低耦合 的思想

在 Golang 中 interface 也是一种数据类型,不需要显示实现。只需要一个变量含有接口类型中的所有方法,那么这个变量就实现了这个接口 ( duck typing ) 。

 type 接口名 interface{ 
     方法名1 (参数列表1) 返回值列表1 
     方法名2 (参数列表2) 返回值列表2 
     ... 
 } 

interface 的特性

Go 语言继承了 静态语言 和 动态语言 对于 接口设计(duck typing) 的优点。

Go 不像大多数静态语言 需要显示的声明类型的继承关系。而 Go没有类,无法像 Java 一样实现显示继承,Go 是通过 interface 实现了类似于 动态语言的 隐式继承 (无需显示的类型继承) 。

虽然动态语言也实现了 隐式继承,但是它们只能在运行是才能检查到类型的转化错误。而 golang 的 interface 特性可以在编译时就能发现错误。

例如:

// 先定义一个接口,和使用此接口作为参数的函数:
type IGreeting interface{
    sayHello()
}

func sayHello(i IGreeting) {
    i.sayHello
}

// 再定义两个结构体:
type Go struct {}
func (g Go) sayHello() {
    fmt.Println("Hi, I am Go!")
}

type Php struct {}
func (p Php) sayHello() {
    fmt.Println("Hi, I am php!")
}

// 最后,在 main 函数中调用 sayHello 函数
func main(){
    golang := Go{}
    php := Php{}
    
    sayHello(golang)  // Hi, I am Go!
    sayHello(php)   // Hi, I am php!
}

在 main 函数中,调用调用 sayHello() 函数时,传入了 golang, php 对象,它们并没有显式地声明实现了 IGreeting 类型,只是实现了接口所规定的 sayHello() 函数。这就是 duck typing 。 实际上,编译器在调用 sayHello() 函数时,会隐式地将 golang, php 对象转换成 IGreeting 类型,这也是静态语言的类型检查功能。

空接口

Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口。

空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。

 type A interface{} // 空接口表示没有任何约束 任意的类型都可以实现空接口 
 func main(){ 
     var a A
     str := "Hello China" 
     a = str // 让字符串实现 a 这个空接口 
     fmt.Println(a) 
     num := 20
     a = num 
     fmt.Println(a) 
}

// 空接口可以直接当做类型来使用,可以表示任意类型 
var a interface{}
a = 20
a = "Hello China"

使用空接口的其他用处:

// 1. 空接口作为参数的参数:可以接收任意类型的函数参数。
func show(a interface{}){
    fmt.Printf("type:%T value:%v\n", a, a)
}

// 2. map 的值实现空接口: 可以保存任意值的字典
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "张三"
studentInfo["age"] = 18
studentInfo["married"] = false

fmt.Println(studentInfo)

// 3. 切片实现空接口
var slice = []interface{}{"1", 1, true}

fmt.Println(slice)

类型转换 和 类型断言

类型转换 和 类型断言 本质都是把一个类型转换成另一个类型。

类型转换:在编译期完成,包括 强制转换 和 隐式转换。

Go 语言要求不同类型之间必须做显式的类型转换。

类型断言:在运行时确定,包括 安全类型断言 和 非安全类型断言

类型转换

对于类型转换而言,转换前后的两个类型要 相互兼容 才行。

类型转换的语法为:

<结果类型> := <目标类型> (<表达式>) 
// 例如
var i int = 9
var f float64
f = float64(i)
fmt.Println("%T-%v") // float64-9

通常来说:小类型 向 大类型转时,是安全的。大类型 向 小类型 转时,则有可能会出现 截断 问题。(因为可能会超出了小类型的内存)

类型断言

因为 空接口 interface{} 中没有定义任何方法,所以 Go 语言中所以类型都实现了空接口。等价于 空接口可以表示任意数据类型。

但是,既然 空接口可以表示任意数据类型, 那如何知道 它 的真实类型 是什么呢?

在 Go 语言中是使用 类型断言 的方式得到 空接口 中的真实类型的。

接口 的转换遵循以下规则:

  1. 普通类型向接口类型的转换是隐式的。

  2. 接口类型向普通类型转换需要类型断言。

断言的语法为:

 // 安全类型断言
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )  

//非安全类型断言
<目标类型的值> := <表达式>.( 目标类型 )

// 例如
type Student struct {
    Name string
    Age int
}

// 非安全类型断言
func main(){
    var i interface{} = new(Student)
    s := i.(Student)  
    fmt.println(s)
}
// 运行时,直接会 panic 。 这是因为 i 是 *Student 类型,并非 Student 类型
// 断言失败。就会 panic

// 安全类型断言
func main(){
    var i interface{} = new(Student)
    s, ok := i.(Student)
    if ok {
        fmt.Println(s)
    }
}
// 这样,即使断言失败也不会 panic。

使用 类型断言 还有一种转换方式:switch 测试。这种方式只能出现在 switch 语句中。

switch 测试:利用 switch 语句判断接口的类型。每一个 case 会被顺序地考虑,当命中一个 case 时,就会执行 case 中的语句。

func main() {
   html := make([]interface{}, 5)
   html[0] = "div"
   html[1] = "span"
   html[2] = []byte("script")
   html[3] = "style"
   html[4] = "head"

   for index, value := range html {
      switch value.(type) {
      case string:
         fmt.Printf("html[%d] is a string and its value is %v\n", index, value)
      case int:
         fmt.Printf("html[%d] is a int and its value is %v\n", index, value)
      case []byte:
         fmt.Printf("html[%d] is a bytes and its value is %s\n", index, value)
      default:
         fmt.Printf("html[%d] unknown type and its value is %v\n", index, value)
      }
   }
}

// 打印结果:
// html[0] is a string and its value is div 
// html[1] is a string and its value is span 
// html[2] is a bytes and its value is script 
// html[3] is a string and its value is style 
// html[4] is a string and its value is head

插一嘴:fmt.Println 函数的参数是 interface。

fmt.Println 的参数 如果是内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。

而如果是自定义类型,首先确定该类型是否实现了 String() 方法,如果实现了,则直接打印输出 String() 方法的结果;否则,会通过反射来遍历对象的成员进行打印。

type Student struct {
   Name string
   Age  int
}

func (s Student) String() string{
   return fmt.Sprintf("[Name: %s], [Age: %v]", s.Name, s.Age)
}

func main() {
   var s = Student{"Asa", 8}
   fmt.Println(s)
}

// 打印结果:
// [Name: Asa], [Age: 8]

值接收者和指针接收者的区别

如果结构体中的方法是 值接收者,那么实例化后的 结构体值类型 和 结构体指针类型都可以赋值给接口变量。

如果结构体中的方法是 指针接收者,那么实例化后 结构体指针类型 可以赋值给接口变量 ,结构体值类型 没法赋值给接口变量。

继续使用上面 fmt.Println 的例子:

type Student struct {
   Name string
   Age  int
}

func (s Student) String() string{
   return fmt.Sprintf("[Name: %s], [Age: %v]", s.Name, s.Age)
}

func main() {
   var s = Student{"Asa", 8}
   fmt.Println(s)
   fmt.Println(&s)
}

// 打印结果
// [Name: Asa], [Age: 8] 
// [Name: Asa], [Age: 8] 

// ---------------------------------
// 而将 String 方法 改成如下时:
func (s *Student) String() string{
   return fmt.Sprintf("[Name: %s], [Age: %v]", s.Name, s.Age)
}

// 打印结果
// {Asa 8} 
// [Name: Asa], [Age: 8]

可以看的:当结构体方法是 值接收者 时,结构体值类型 和 结构体指针类型 都能调用这个方法。

而 结构体方法是 指针接收者 时,只能由 结构体指针类型 调用。

同时,当结构体方法时 值接收者 时,在方法中改变变量的值,并不会改变原变量的值。

而 结构体方法是 指针接收者 时,在方法中改变变量的值,并将会改变原变量的值。

type Student struct {
   Name string
   Age  int
}

func (s Student) String() string{
   s.Age = 12
   return fmt.Sprintf("[Name: %s], [Age: %v]", s.Name, s.Age)
}

func main() {
   var s = Student{"Asa", 8}
   fmt.Println(&s)
   fmt.Println(s.Age)
}


// 打印结果
// [Name: Asa], [Age: 12] 
// 8

// ---------------------------------
// 而将 String 方法 改成如下时:
func (s *Student) String() string{
   return fmt.Sprintf("[Name: %s], [Age: %v]", s.Name, s.Age)
}

// 打印结果
// [Name: Asa], [Age: 12] 
// 12

eface和iface区别

在 Go 语言中,interface 在底层的内部结构是分为两个结构体:eface 和 iface 。

  • eface 指的是没有方法的 interface。
  • iface 就指的是有方法的 interface

接下来,就从源码的层面来重新了解一下 interface。

type iface struct {
    tab *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype     // 接口定义的类型信息 描述了接口的类型
    _type  *_type             // 接口实际指向值的类型信息 描述了实体的类型,包括内存对齐方式,大小等
    hash   uint32 // copy of _type.hash. Used for type switches.    
    _      [4]byte  
    fun    [1]uintptr // variable sized 放置与 接口方法 对应的具体数据类型的方法地址
}

iface 之所以它是包含方法的 interface,关键就在于 itab的数据结构 。

itab 主要包含两部分:

  • 确定唯一的包含方法的 interface 的具体结构类型:通过 inter 以及 _type 确定
  • 指向具体方法集的指针:通过 fun 确定

插一嘴

若是 接口 定义了多个方法,itab 中的 fun 数组大小为 1 够用么?

够用。实际上,fun 只存储 第一个方法的函数指针,如果有更多的方法,将在它之后的内存空间里继续存储。

接下来,看eface的结构:

type eface struct {    
    _type *_type    
    data  unsafe.Pointer
}

eface 就相对简单一些。_type 存储类型信息;data 存储类型的值的指针,指向实际值或者实际值的拷贝。

最后,再来看一下 _type 结构体

type _type struct {
   size       uintptr // 类型大小
   ptrdata    uintptr // 存储器前缀的大小
   hash       uint32  // 类型的 hash 值
   tflag      tflag   // 类型的 flag,和反射相关
   align      uint8   // 内存对齐相关
   fieldAlign uint8
   kind       uint8   // 类型的编号,有bool, slice, struct 等等等等
   // 用于比较该类型对象的函数
   // (ptr to object A, ptr to object B) -> ==?
   equal func(unsafe.Pointer, unsafe.Pointer) bool
   // gcdata 存储了垃圾收集器的 GC 类型数据。
   // If the KindGCProg bit is set in kind, gcdata is a GC program.
   // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
   gcdata    *byte
   str       nameOff
   ptrToThis typeOff
}

Go 语言各种数据类型都是在 _type 字段的基础上,增加一些额外的字段进行管理的。这些数据类型的结构体定义,是反射实现的基础:

type arraytype struct {
   typ   _type
   elem  *_type
   slice *_type
   len   uintptr
}

type chantype struct {
   typ  _type
   elem *_type
   dir  uintptr
}

type slicetype struct {
   typ  _type
   elem *_type
}

type functype struct {
   typ      _type
   inCount  uint16
   outCount uint16
}

type ptrtype struct {
   typ  _type
   elem *_type
}

type structfield struct {
   name       name
   typ        *_type
   offsetAnon uintptr
}

接口的动态类型和动态值

从上面的接口底层源码中可以看到: iface 包含两个字段:tab 是接口表指针,指向类型信息;data 是数据指针,则指向具体的数据。它们分别被称为动态类型和动态值。所以接口值包括动态类型和动态值这两部分。

接口值的零值 是指 动态类型 和 动态值 都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值才会被认为 接口值 == nil。

package main
import "fmt"

type Coder interface {
    code()
}
type Gopher struct {
    name string
}
func (g Gopher) code() {
    fmt.Printf("%s is coding\n", g.name)
}

func main() {
    var asa Coder    
    fmt.Println(asa == nil)                  // true
    fmt.Printf("asa: %T, %v\n", c, c)        // asa: <nil>, <nil>
    
    var gopher *Gopher    
    fmt.Println(gopher == nil)               // true
    asa = gopher    
    fmt.Println(asa == nil)                  // false
    fmt.Printf("asa: %T, %v\n", c, c)        // asa: *main.Gopher, <nil>
}

asa 的 动态类型 和 动态值 都为 nil*,gopher 也为 nil,当把* gopher 赋值给 asa 后,asa 的动态类型变成了 main.Gopher,尽管 asa 的动态值仍为 nil,但是当* asa 和 nil 作比较的时候,结果却是 false 。

多态

多态是一种运行期的行为,它有以下几个特点:

  1. 一种类型具有多种类型的能力
  2. 允许不同的对象对同一消息做出灵活的反应
  3. 以一种通用的方式对待个使用的对象
  4. 非动态语言必须通过继承和接口的方式来实现
type Table interface {
   Add()
   Select()
}

// Blog_post 是博客信息
type Post struct {
   ID           int
   Title        string
   Content      string
}

func (p Post)Add()  {
   fmt.Printf("文章名为 %s 的文章已添加成功!\n", p.Title)
}

func (p Post)Select()  {
   fmt.Printf("文章 ID 为 %d 的内容是 %s\n", p.ID, p.Content)
}

type Tag struct {
   ID           int
   Name        string
}

func (t Tag)Add()  {
   fmt.Printf("标签名为 %s 的标签已添加成功!\n", t.Name)
}

func (t Tag)Select()  {
   fmt.Printf("标签 ID 为 %d 的名称是 %s\n", t.ID, t.Name)
}

func main() {
   p := Post{ID: 1,Title: "Go 中的 interface 用法"}
   t := Tag{ID: 2,Name: "Golang"}

   tables := []Table{p,t}

   for _, table := range tables {
      table.Add()
      table.Select()
   }
}

//打印输出:
//文章名为 Go 中的 interface 用法 的文章已添加成功! 
//文章 ID 为 1 的内容是 
//标签名为 Golang 的标签已添加成功! 
//标签 ID 为 2 的名称是 Golang 

接口编程

type Table interface {
   Add()
   Select()
}

// Blog_post 是博客信息
type Post struct {
   ID           int
   Title        string
   Content      string
}

func (p Post)Add()  {
   fmt.Printf("文章名为 %s 的文章已添加成功!\n", p.Title)
}

func (p Post)Select()  {
   fmt.Printf("文章 ID 为 %d 的内容是 %s\n", p.ID, p.Content)
}

type Tag struct {
   ID           int
   Name        string
}

func (t Tag)Add()  {
   fmt.Printf("标签名为 %s 的标签已添加成功!\n", t.Name)
}

func (t Tag)Select()  {
   fmt.Printf("标签 ID 为 %d 的名称是 %s\n", t.ID, t.Name)
}

func AddTable(table Table) {
   table.Add()
}

func SelectTable(table Table){
   table.Select()
}

func main() {
   p := Post{ID: 1,Title: "Go 中的 interface 用法"}
   t := Tag{ID: 2,Name: "Golang"}

   AddTable(p)
   SelectTable(t)
}

上面这段代码,可以看到——使用 Table 的接口,把 “业务类型” Post 和 Tag 与 “控制逻辑” AddTable**()** 和 SelectTable() **给解耦了。**于是,只要实现了 Table 接口,都可以传给AddTable() 和 SelectTable() 来使用。

小技巧:接口完整性检查

Go 语言的编程器并没有严格检查一个对象是否实现了某接口所有的接口方法,如下面这个示例:

type Shape interface {
    Sides() int
    Area() int
}
type Square struct {
    len int
}
func (s* Square) Sides() int {
    return 4
}
func main() {
    s := Square{len: 5}
    fmt.Printf("%d\n",s.Sides())
}

可以看到 Square 并没有实现 Shape 接口的所有方法,程序虽然可以跑通,但是这样编程的方式并不严谨,如果需要强制实现接口的所有方法,那么应该怎么办呢?

在 Go 语言编程圈里有一个比较标准的作法:

// 检查 *Square 类型是否实现了 Shape 接口
var _ Shape = (*Square)(nil)
// 检查 Square 类型是否实现了 Shape 接口
var _ Shap = Squart{}

声明一个 _ 变量,其会把一个nil的空指针,从Square转成Shape,这样,如果没有实现完相关的接口方法,编译器就会报错:

cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)

这样就做到了个强验证的方法。

插一嘴:补充一点:(与检验接口完整性的代码要分清楚)

var i interface{} = (*Student)(nil)
// 意思为:将 i 的动态类型设为 (*Student), 数据为 nil

END