Golang 复合数据类型:接口

接口

Golang 不是传统的面向对象编程语言,它没有类和继承的概念,但是有非常灵活的接口概念,通过它可以实现很多面向对象的特性,接口提供了一种方式来说明这样的行为:如果谁能搞定这件事,它就可以在这里调用。接口定义了一组方法(方法集),这些方法都是抽象的,即不包含实现这些方法的代码,接口也不包含变量。Golang 中的接口方法不会很多,一般只有0~3个方法;而且 Golang 是接口可以有值,表现为一个接口变量或者一个值

//声明接口类型speaker
//不管是什么类型,只要实现了speak方法,都可认为是接口类型
type speaker interface {
    speak()  
}

//声明三个具体实现接口方法的结构体

type cat struct {
}
func (c cat)speak()  {
    fmt.Println("喵喵喵")
}

type dog struct {
}
func (d dog)speak(){
    fmt.Println("汪汪汪")
}

type person struct {
}
func (p person )speak(){
    fmt.Println("Hello Go!")
}

//接收接口类型的变量speaker,实现speaker的函数
func attack(p speaker){
    p.speak()
}
//调用
func main(){
    var c1 cat
    var d1 dog
    var p1 person
    
    attack(c1)
    attack(d1)
    attack(p1)
}

/*
喵喵喵
汪汪汪
Hello Go!
*/

代码说明:

  • 声明一个接口类型speaker,按照约定,只包含一个方法的接口,它的名字由方法名加[e]r后缀组成。例如接口中只有一个方法speak(),所以接口名为speaker
  • 对于接口类型speaker,只要是实现了接口定义的speak()方法,都可被认为是接口类型,即实现接口
  • 声明三个具体实现接口方法的结构体struct,分别是catdogperson,每一个struct都包含了具体实现接口方法speak()的方法,这些方法具体实现了打印猫、狗和人的叫声
  • 声明一个实现接口的函数attack,这个方法接收一个接口类型为speakr的变量
  • main函数中,声明变量c1d1p1,调用attack方法,然后传入三个变量实现speaker接口

侵入式接口

在传统的面向对象编程语言中(如 Java 和 C++)实现类需要明确声明自己实现了interface接口,就叫“侵入式接口”。

interface IFoo{
    void Bar();
}
class Foo implements IFoo{    //java
    //...
}
class Foo : public IFoo {   //C++
  // ...
}
IFoo* foo = new foo;

代码说明:

  • 声明接口IFoo,定义一个Bar()方法
  • 声明类Foo,这个类需要显式地指定自己实现了接口IFoo

非侵入式接口

在 Golang 中,任意类型struct只要实现了该方法,我们就认为这个类型实现了这个接口。这个不需要在定义类型的时候指定该类型所要实现的接口的方式被称为“非入侵式的接口”。

type IFoo interface { //声明接口,包含一个Bar()方法
    Bar()
}

type Foo struct { //声明struct类型
}
 
func(f *Foo) Bar(){ //声明具体实现接口的方法
    f.Bar() 
}

代码说明:

  • 声明接口IFoo,包含一个抽象的Bar()方法
  • 声明结构体类型Foo
  • 声明类型Foo具体实现接口的方法

不过,非侵入式接口带来的缺点也很明显:

  • 性能下降。使用interface作为函数参数,运行时runtime会动态的确定行为;如果使用结构体(类)struct作为函数参数,编译期间就可以确定了。
  • 不能更清晰地知道哪些struct实现了哪些interface,代码可读性下降。解决办法:使用 guru 工具

空接口

空接口或者最小接口 不包含任何方法,它对实现不做任何要求:

type Any interface {}

练习(demo)

接口变量var被依次赋予一个intstringPerson结构体实例的值,然后使用类型判断type-switch来测试它们的实际类型。每个interface{}变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储包含它的值或指向值的指针。

var i = 5
var str = "ABC"
//声明类
type Person struct {
    name string
    age  int
}
//声明空接口
type Any interface{}

func main() {
    var val Any
    val = 5
    fmt.Printf("val has the value: %v\n", val)
    val = str
    fmt.Printf("val has the value: %v\n", val)
    //实例化
    pers1 := new(Person)
    pers1.name = "Xiao ma"
    pers1.age = 18
    val = pers1
    fmt.Printf("val has the value: %v\n", val)
    //类型判断
    switch t := val.(type) {
    case int:
        fmt.Printf("Type int %T\n", t)
    case string:
        fmt.Printf("Type string %T\n", t)
    case bool:
        fmt.Printf("Type boolean %T\n", t)
    case *Person:
        fmt.Printf("Type pointer to Person %T\n", t)
    default:
        fmt.Printf("Unexpected type %T", t)
    }
}

/*
val has the value: 5
val has the value: ABC
val has the value: &{Xiao ma 18}
Type pointer to Person *main.Person
*/

在匿名函数使用空接口作为参数的例子:

type specialString string
var whatIsThis specialString = "str"

func TypeSwitch() {
    //空接口作为匿名函数的参数
    testFunc := func(any interface{}) {
        //类型判断
        switch v := any.(type) {
        case bool:
            fmt.Printf("Any %v is a bool type", v)
        case int:
            fmt.Printf("Any %v is an int type", v)
        case float32:
            fmt.Printf("Any %v is a float32 type", v)
        case string:
            fmt.Printf("Any %v is a string type", v)
        case specialString:
            fmt.Printf("any %v is a special String!", v)
        default:
            fmt.Println("unknown type!")
        }
    }
    testFunc(whatIsThis)
}
//函数调用
func main() {
    TypeSwitch()
}

/*
any str is a special String!
*/

### 鸭子类型

(待补充)

练习(demo)

完满达人小马考完研没事干,于是天天在街上溜达,不料碰上了一只Monster,小马不畏强暴,使出了百万吨拳击打败了这只Monster……(有空再把完善业务逻辑,先写个简单的demo巩固一下今日所学知识QAQ)

import (
    "fmt"
    "math"
)
//声明接口Shaper
// 定义抽象方法Area(),只要实现接口方法的变量,都可认为是该接口类型,即实现接口
type Shaper interface {
    Area()
}
//声明小马类,定义字段名和字段类型
type Xiaoma struct {
    blood float64
}
//声明实现接口的方法
func (x *Xiaoma)Area() float64{
    return x.blood
}
//声明怪兽类,定义字段名和字段类型
type Monster struct {
    blood float64
}
//声明实现接口的方法
func (m Monster)Area()float64 {
    return m.blood
}
//主函数调用
func main(){
    //实例化
    x1 := new(Xiaoma)
    m1 := Monster{}
    x1.blood = 85  //初始化小马的生命值
    m1.blood = 100 //初始化怪兽的生命值
    if x1.blood <= 60 {
        fmt.Println("小马失血过多!")
    }else {
        sq := math.Sqrt(m1.blood) 
        fmt.Println("小马使出百万吨拳击!")
        fmt.Printf("怪兽剩余生命值:%v\n",sq)
    }
    areaInt := x1
    fmt.Printf("小马剩余生命值:%v\n",areaInt.Area())
}

/*
小马使出百万吨拳击!
怪兽剩余生命值:10
小马剩余生命值:85
*/

代码说明:

  • 引入fmtmath包,为了防止精度丢失,字段类型统一使用float64
  • 声明接口Shaper,定义抽象方法Area(),只要实现接口方法的变量,就是实现接口
  • 声明小马类XiaomaMonster,定义字段名和字段类型
  • 对于小马类Xiaoma和怪兽类Monster,声明实现接口的方法Area(),接收者类型分别为指针*Xiaoma和值Monster
  • 在主函数实例化小马类Xiaoma和怪兽类Monster,初始化成员变量x1.blood是85和m1.blood是100
  • if-else判断,当小马生命值小于60时,则输出“小马失血过多!”;否则,小马将对怪兽使出百万吨拳击,该绝招的计算公式为对一个数求平方根,输出怪兽的生命值
  • 调用接口方法Area(),输出小马此时的生命值!

接口类型与约定

接口类型实现上是描述了一些列方法的集合。一个实现了这些方法的具体类型是这个接口类型的实例。io包定义了很多其他有用的接口类型,例如,io.Write类型是用得最广泛的接口之一,它提供了所有的类型写入[]byte(字节)的抽象;ip.Reader可以代表任意可读取byte类型,io.Close可以是任意可关闭的值。

接口内嵌

和结构体内嵌相似,接口也可以内嵌于另一个接口中,而不需要声明另一个接口(被内嵌接口)的所有方法,缺点是代码简洁性不佳。例如,将ReaderWriter接口内嵌于ReaderWriterCloser接口中:

  • 传统的声明接口
type ReaderWriter interface{
    Reader
    Writer
}
//接口内嵌
type ReaderWriterCloser interface{
    Reader
    Writer
    Closer
}
  • 使用io.Writer类型声明接口(不使用内嵌)
type ReaderWriter interface{
    Read (p []byte) (n int, err error)
     Writer (p []byte) (n int, err error)
}
  • 使用混合风格声明接口
type ReaderWriter interface{
    Read(p []byte) (n int, err error)
    Writer
}

类型转换

由于 Go 语言是强类型的静态编程语言,并不允许隐式转换类型的理念,因此在互不兼容的类型之间需要进行类型转换。类型转换(强制)的语法格式为:<结果类型> := <目标类型> (<表达式>)

func main(){
    //类型声明
    var var1 float64 = 3.14159265359
    fmt.Printf("var1:%v\n",var1)
    //强制类型转换(语法格式)
    var2 := float32(var1)
    var3 := int64(var1)
    fmt.Printf("var2:%v\nvar3:%v\n",var2,var3)
}

/*
var1:3.14159265359
var2:3.1415927
var3:3
*/

类型断言

类型断言:接口类型向普通类型的转换过程就叫类型断言(运行时确定),这是Go语言内置的一种智能推断类型的功能。一切隐式转换都被视为是不安全的,因此在 Go 语言中,必须要使用显式类型转换确保这是一种安全的方式。

一个类型断言表达式的语法为i.(T),其中i为一个接口值, T为一个类型名或者类型字面表示。 类型T可以为任意一个非接口类型,或者一个任意接口类型。

  • 未经检查的类型断言(不安全):<目标类型的值> := <表达式>.( 目标类型 )
func main(){
        var i interface{} = "sick" //未经检查且错误的类型断言
        j := i.(int)
        fmt.Printf("%T->%d\n", i, j)
}

/*
panic: interface conversion: interface {} is string, not int
*/

代码说明:

  • var i interface{} = "sick" 相当于让编译器自动推断出接口类型是string
  • 使用强制类型转换的语法将变量i转换成int型`
  • 编译器会进行静态类型检查,显示这种转换是失败的,因此调用内置的panic()函数,抛出一个异常

解决办法:

  • "sick"改成整型值1或者将目标类型改为string,这种办法虽然能使编译器通过,但这种方法依旧是不安全的。
func main(){
        var i interface{} = 1 //未经检查的类型断言
        j := i.(int)
        fmt.Printf("%T->%d\n", i, j)
}
//or
func main(){
        var i interface{} = "sick" //未经检查且错误的类型断言
        j := i.(string)
        fmt.Printf("%T->%d\n", i, j)
}
  • 已检查的类型断言(安全):<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )
func main(){
        var i interface{} = "TT"
        //安全的类型断言
        j, b := i.(int)
        if b { //true
            fmt.Printf("%T->%d\n", i, j)
        } else {//false
            fmt.Println("类型不匹配")
        }
}
/*
类型不匹配
*/

代码说明:

  • 可以将"TT"改成整型值1,这时布尔类型b的值为true,将执行循环体的语句,输出i的类型为int

练习(demo)

这是类型断言中的一个特殊的例子,假设v是一个值,我们希望测试它是否实现了PPP接口,可以这样做:

type V1 struct {
    name string
}
func (v *V1) Print() {
    fmt.Printf("V1 name=%s\n", v.name)
}

type V2 struct {
    score int
}
func (v *V2) PPrint() {
    fmt.Printf("V2 score=%d\n", v.score)
}

func main() {
    type P interface {
        Print()    // 需要是否实现了Print函数
    }

    type PP interface {
        PPrint()  // 需要是否实现了PPrint函数
    }

    T := func(t interface{}) {  // 判断函数
        if v, ok := t.(P); ok {   // 如果实现了P接口,也就是判断是否实现了Print函数
            v.Print()   // 判断后可以根据需要实现代码
        } else if vv, ok2 := t.(PP); ok2 {  // 如果实现了PP接口,也就是判断是否实现了PPrint函数
            vv.PPrint()  // 判断后可以根据需要实现代码
        }
    }

    // 以下为测试代码
    v1 := &V1{"Jack"}
    v2 := &V2{100}

    T(v1)
    T(v2)
}

类型判断

接口变量的类型也可以由于一组特殊形式的type-switch语句来判断。例如,有一个可变长度参数的数组,可以是任意类型,它会根据数组元素的不同类型执行不同的动作:

func classify(items ...interface{}){
    for i,x := range items{
        switch x.(type) {
        case bool:
            fmt.Printf("Param #%d is a bool\n",i)
        case int:
            fmt.Printf("Param #%d is a int\n",i)
        case float32:
            fmt.Printf("Param #%d is a float32\n",i)
        case string:
            fmt.Printf("Param #%d is a string\n",i)
        default:
            fmt.Printf("Param #%d is a unknown\n",i)
        }
    }
}

/*
Param #0 is a int
Param #1 is a int
Param #2 is a unknown
Param #3 is a string
Param #4 is a bool
*/

可以在main函数中调用类型判断的方法:classify(13,1,3.4678833422235425435,"小马",false),打印类型判断结果。在处理来自外部的、类型未知的数据时,比如解析JSONXML编码的数据文件,使用类型判断type-switch十分有用。


发表评论

电子邮件地址不会被公开。 必填项已用*标注