go接口

65 阅读8分钟

在Go语言中接口(interface)是一种类型,一种抽象的类型。

我们编写一个猫,一个狗的结构体。

然后编写一个吃饭的函数,可以接收,猫或者狗的结构体,让猫的表现行为吃鱼,狗的表现行为为吃骨头

用传统的方法,无法实现一个参数既是猫也是狗

此时我们就需要把他们的公共方法提取到接口。在定义一个吃饭的接口

type cat struct{}

type dog struct{}

//错误:func meal(c cat|dog) { fmt.Println("猫|狗~") }

例子代码:

​编辑

2.接口细节

接口中只可以定义方法

type 接口名 interface{

方法名1(参数1,参数2...)(返回值1,返回值2...)

方法名2(参数1,参数2...)(返回值1,返回值2...)

}

变量,参数,返回值可以设置为接口类型

Go 中没有关键字显式声明某个类型实现了某个接口。只要一个类型实现了接口要求的所有方法,该类型就自动被认为实现了该接口。

空接口定义为 interface{},可以表示任何类型。

一个未初始化的接口变量其值为 nil,且不包含任何动态类型或值。

了解

在Go的内部,一个接口通常由两个字段组成:

  1. 类型信息(Type):这是一个指向类型描述符的指针,包含了接口的具体类型信息。

  2. 数据(Value):这是一个指向实际数据的指针,这个数据是实现接口的对象的实际数据。

​编辑

3.结构体,和结构体指针实现接口的区别

我们修改例子代码,让狗的指针实现接口方法

此时,狗的结构体不能当做接口。提示(变量狗的类型,没有实现eat方法,方法eating是一个指针的接受者)

对于猫的结构体实现接口,编译器在接收到指针时候,给我们做了优化,在实际调用时候,给我们加上了指向。

​编辑

总结​编辑

package main

import "fmt"


type eat interface {
	change()
}
type cat struct {
	id   int
	name string
}

func (c cat) change() {
	c.name = "修改"
	fmt.Println("我是方法中的猫", c)
}

func runMyf(e eat) {
	e.change()
}
func main() {
	var c = &cat{
		1, "小花",
	}
	runMyf(c) //传递的是指针,实际接收是结构体,
	// 此时编译器相当于给我们调用了runMyf(*c)
	fmt.Println(c)

	//打印结果:我是方法中的猫 {1 修改}
	//&{1 小花}
}

接口可以嵌套

type eat interface {

    eating()

}

type speek interface {

    speaking()

}

type animal interface {

    eat // eat接口嵌入到animal接口

    speek // speek接口嵌入到animal接口

}

type cat struct{}



func (c cat) eating() {

    fmt.Println("猫吃鱼")

}

func (c cat) speaking() {

    fmt.Println("猫喵喵叫")

}

func desc(a animal) {

    a.eating()

    a.speaking()

}

func main() {

    var m cat

    desc(m)

}

接口断言

接口类型的变量.(要判断的实际类型)

​编辑

在swich中,接口类型要判断的实际类型可以接收type。用于判断接口是哪种实际类型

​编辑

接口与多态

Go 语言通过接口实现多态,允许不同的结构体实现相同的接口方法

package main
import "fmt"
// 定义一个接口
type Speaker interface {
    Speak()
}
// Animal结构体实现Speaker接口
type Animal struct {
    Name string
}
func (a Animal) Speak() {
    fmt.Println(a.Name, "makes a sound.")
}
// Dog结构体实现Speaker接口
type Dog struct {
    Animal
    Breed string
}
func (d Dog) Speak() {
    fmt.Println(d.Name, "barks.")
}
// Cat结构体实现Speaker接口
type Cat struct {
    Name string
}
func (c Cat) Speak() {
    fmt.Println(c.Name, "meows.")
}
func main() {
    var s Speaker
    a := Animal{Name: "Generic Animal"}
    d := Dog{Animal: Animal{Name: "Buddy"}, Breed: "Golden Retriever"}
    c := Cat{Name: "Whiskers"}
    s = a
    s.Speak() // 输出: Generic Animal makes a sound.
    s = d
    s.Speak() // 输出: Buddy barks.
    s = c
    s.Speak() // 输出: Whiskers meows.
}

在这个示例中,AnimalDogCat 都实现了 Speaker 接口的 Speak 方法。通过接口类型 Speaker,可以存储任何实现了该接口的结构体,实现多态行为。

​​

空接口(Empty Interface)​​ 是一种特殊的接口类型,它不包含任何方法。由于在 Go 中,任何类型都至少实现了零个方法,因此所有类型都隐式地实现了空接口。这使得空接口可以持有任何类型的值,类似于其他编程语言中的通用类型(如 Object 在 Java 中)

空接口的定义

空接口使用一对空的括号 interface{} 来表示:

var i interface{}

这里的 i 可以存储任何类型的值,因为所有类型都满足空接口的要求。

go 内置了 any 类型。该类型实际上是指interface{}类型,这是一种空接口类型,可以接受任何类型的参数

空接口的使用场景

  1. 存储任意类型的值:当你需要一个变量能够存储不同类型的值时,可以使用空接口。

  2. 作为函数参数:当函数需要接受任意类型的参数时,可以使用空接口作为参数类型。

  3. 实现泛型之前的通用编程:在 Go 1.18 引入泛型之前,空接口常用于编写通用的函数和数据结构。

    package main

    import ( "fmt" )

    func main() { var i interface{}

    // 存储整数
    i = 42
    fmt.Println(i) // 输出: 42
    
    // 存储字符串
    i = "Hello, Go!"
    fmt.Println(i) // 输出: Hello, Go!
    
    // 存储浮点数
    i = 3.14
    fmt.Println(i) // 输出: 3.14
    
    // 存储布尔值
    i = true
    fmt.Println(i) // 输出: true
    

    }

作为函数参数

package main

import (
    "fmt"
)

func printAnything(value interface{}) {
    fmt.Println(value)
}

func main() {
    printAnything(100)
    printAnything("Go is awesome!")
    printAnything(2.718)
    printAnything(true)
}

类型断言(Type Assertion)

由于空接口可以存储任何类型的值,在使用时通常需要将其转换回具体的类型。这可以通过类型断言来实现。

基本用法

value, ok := i.(Type)

  • value 是断言成功后的具体类型的值。
  • ok 是一个布尔值,表示断言是否成功。

如果断言失败且没有使用 ok,程序会发生运行时错误(panic)。

package main

import (
    "fmt"
)

func main() {
    var i interface{} = "Hello, Go!"

    // 使用类型断言
    s, ok := i.(string)
    if ok {
        fmt.Println("断言成功:", s)
    } else {
        fmt.Println("断言失败")
    }

    // 尝试断言为整数
    n, ok := i.(int)
    if ok {
        fmt.Println("断言成功:", n)
    } else {
        fmt.Println("断言失败") // 这里会输出
    }

    // 不使用ok,断言失败会引发panic
    // fmt.Println(i.(int)) // 运行时错误: interface conversion: interface {} is string, not int
}

类型切换(Type Switch)

当需要根据接口值的具体类型执行不同的逻辑时,可以使用类型切换(Type Switch)​

func doSomething(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("处理整数: %d", v)
    case string:
        fmt.Printf("处理字符串: %s", v)
    case float64:
        fmt.Printf("处理浮点数: %f", v)
    default:
        fmt.Printf("未知类型: %T", v)
    }
}

func main() {
    doSomething(10)
    doSomething("Hello")
    doSomething(3.14)
    doSomething(true)
}

输出:​

处理整数: 10
处理字符串: Hello
处理浮点数: 3.140000
未知类型: bool

空接口的优缺点

优点

  1. 通用性强:空接口可以存储任何类型的值,适用于需要高度灵活性的场景。
  2. 简化代码:在某些情况下,使用空接口可以减少代码的复杂性,尤其是在编写通用函数或数据结构时。

缺点

  1. 类型安全性差:由于空接口可以存储任何类型,编译器无法在编译时检查类型错误,容易导致运行时错误。
  2. 性能开销:使用空接口和类型断言会带来一定的性能开销,尤其是在高性能需求的场景下。
  3. 代码可读性降低:过多使用空接口可能使代码难以理解和维护,因为具体的类型信息被隐藏了。

使用建议

  • 尽量减少使用空接口:在明确知道具体类型的情况下,优先使用具体类型,以保证类型安全和代码的可读性。
  • 必要时使用空接口:在需要编写通用函数或数据结构(如切片、映射等)且类型不确定时,可以考虑使用空接口。
  • 结合类型断言或类型切换:在使用空接口时,合理使用类型断言或类型切换来恢复具体的类型信息,确保程序的正确性。