目录
接口
接口介绍
接口定义
实现接口
空接口
实现空接口
类型断言
使用类型断言
结构体实现多接口
接口嵌套
结构体值接收者和指针接收者实现接口的区别
代码案例
接口
接口介绍
Go语言中的接口(interface)是一组方法签名的集合,是一种抽象类型。接口定义了方法,但没有实现,而是由具体的类型(struct)实现这些方法,因此接口是一种实现多态的机制。
接口定义
Go语言中的接口定义语法如下:
type 接口名 interface {
方法名1(参数1 类型1, 参数2 类型2) 返回值类型1
方法名2(参数3 类型3) 返回值类型2
...
}
其中,接口名是一个标识符,方法名是一个标识符,参数和返回值都是类型。一个接口可以包含多个方法,每个方法的返回值类型可以是单个值、多个值、无返回值(void)或者是另一个接口类型。
实现接口
package main
import "fmt"
type animal interface {
sound() string
}
type cat struct {
name string
}
func (c cat) sound() string {
return "meow"
}
type dog struct {
name string
}
func (d dog) sound() string {
return "woof"
}
func makeSound(a animal) {
fmt.Println(a.sound())
}
func main() {
c := cat{"Tom"}
d := dog{"Fido"}
makeSound(c)
makeSound(d)
}
定义了一个 animal
接口,它包含了一个 sound()
方法,没有任何参数,返回一个字符串。然后我们定义了两个类型,一个是 cat
,一个是 dog
,它们都实现了 sound()
方法。最后我们定义了一个 makeSound()
函数,它接收一个实现了 animal
接口的对象,然后调用它的 sound()
方法打印出声音。在 main()
函数中,我们创建了一个 cat
和一个 dog
对象,然后分别传递给 makeSound()
函数进行测试。
实现接口的关键在于方法名和参数列表的匹配。只有当一个类型实现了一个接口中所有的方法,才能称之为这个接口的实现类型。在上面的例子中,cat
和 dog
都实现了 animal
接口中的 sound()
方法,因此它们都是 animal
接口的实现类型。
需要注意的是,Go语言中的接口是隐式实现的,也就是说,只要一个类型实现了接口中定义的所有方法,它就自动成为这个接口的实现类型,不需要显式地声明实现了哪个接口。这使得Go语言中的接口更加灵活,也更加容易使用。
空接口
在 Go 语言中,空接口是指一个没有任何方法的接口。空接口不仅没有方法,也没有任何需要实现的约束条件,因此可以被任何类型实现。因此,空接口也被称为万能接口。
空接口在 Go 语言中的作用非常广泛,可以用来表示任何类型的数据。在许多场景中,我们可能需要处理不同类型的数据,但是又不想为每个类型都定义一个新的接口,这时候就可以使用空接口。
实现空接口
package main
import (
"fmt"
)
// 空接口
type EmptyInterface interface {
}
func main() {
// 可以用空接口表示任何类型的数据
var data1 EmptyInterface = 10
var data2 EmptyInterface = "hello"
var data3 EmptyInterface = []int{1, 2, 3}
fmt.Println(data1)
fmt.Println(data2)
fmt.Println(data3)
}
在上面的示例中,定义了一个名为 EmptyInterface
的空接口。接着,在 main
函数中定义了三个变量 data1
、data2
和 data3
,它们都是 EmptyInterface
类型的变量,并分别赋值为整型、字符串和整型切片。可以看到,在输出这三个变量时,都直接使用了空接口类型的变量。
类型断言
在Go语言中,可以使用类型断言(type assertion)来判断一个接口实例的底层值是什么类型,并将其转换成对应的类型。类型断言的语法如下:
value, ok := interfaceVar.(Type)
其中,interfaceVar
是一个接口变量,Type
是一个具体的类型。如果 interfaceVar
的底层值是 Type
类型,则类型断言返回 interfaceVar
的底层值和 true
;否则返回零值和 false
。
使用类型断言
package main
import (
"fmt"
)
func main() {
var i interface{}
i = "hello"
// 使用类型断言判断 i 的底层值是否为字符串类型
if s, ok := i.(string); ok {
fmt.Printf("i is a string: %s\n", s)
} else {
fmt.Println("i is not a string")
}
// 使用类型断言判断 i 的底层值是否为整数类型
if n, ok := i.(int); ok {
fmt.Printf("i is an integer: %d\n", n)
} else {
fmt.Println("i is not an integer")
}
}
在上面的示例程序中,首先定义了一个空接口变量 i
,并将其赋值为字符串 "hello"
。然后,通过两次类型断言分别判断 i
的底层值是否为字符串类型和整数类型,最终输出判断结果。
输出结果为:
i is a string: hello
i is not an integer
需要注意的是,在使用类型断言时,如果底层值不是指定类型,则会触发运行时错误。因此,在使用类型断言时,通常会将其与条件语句配合使用,以避免出现运行时错误。而且类型断言一般使用在switch语句中。
结构体实现多接口
在Go语言中,结构体可以实现一个或多个接口,这使得结构体可以具备多个不同的行为。
首先定义一个结构体类型:
type Person struct {
name string
age int
}
接着定义两个接口类型:
type Talker interface {
Talk() string
}
type Runner interface {
Run() string
}
Person结构体要实现这两个接口,需要实现它们的方法:
func (p Person) Talk() string {
return "Hi, my name is " + p.name
}
func (p Person) Run() string {
return "I can run " + strconv.Itoa(p.age) + " miles per hour"
}
这里的Talk()方法返回一个字符串,表示Person的自我介绍。Run()方法返回一个字符串,表示Person的奔跑速度。
最后可以创建一个Person实例,并将其赋值给Talker和Runner类型的变量:
func main() {
p := Person{"Tom", 20}
var t Talker = p
var r Runner = p
fmt.Println(t.Talk())
fmt.Println(r.Run())
}
在这个示例中,我们将p赋值给t和r。t是Talker类型,因此它只能访问Talk()方法;r是Runner类型,因此它只能访问Run()方法。但是,由于Person实现了这两个接口,t和r都可以访问Talk()和Run()方法。
接口嵌套
Go语言中的接口嵌套是一种将多个接口组合成一个新接口的方法。它可以让程序员更灵活地组织和复用代码。
接口嵌套的语法格式为:一个接口类型可以嵌套多个接口类型,也可以嵌套一个包含多个接口类型的接口。
嵌套接口的语法格式为:
type Embed interface {
I1
I2
}
其中,I1
和I2
是已经定义好的接口类型。Embed
接口嵌套了I1
和I2
两个接口类型,也就是说,Embed
接口继承了I1
和I2
两个接口的所有方法。这样,实现了Embed
接口的类型也必须实现I1
和I2
接口的所有方法。
接口嵌套可以实现代码的复用和组合。通过嵌套多个接口类型,可以定义一个新的接口类型,这个新的接口类型拥有多个接口类型的方法集合,可以更方便地调用这些方法。同时,由于接口类型的动态特性,我们可以在运行时动态地组合不同的接口类型,进一步实现代码的复用和扩展。
结构体值接收者和指针接收者实现接口的区别
1.在 Go 语言中,结构体可以通过实现接口来满足某个接口的约束条件。在实现接口时,可以使用值接收者或指针接收者。它们的主要区别在于如何处理结构体的拷贝和指针。
2.值接收者方法接收一个结构体值的副本作为接收者,而指针接收者方法接收一个结构体指针作为接收者。具体来说,值接收者方法会将结构体值拷贝一份,然后对副本进行操作,而指针接收者方法则直接操作原始结构体指针所指向的对象。
3.在实现接口时,使用值接收者方法和指针接收者方法的区别在于,使用值接收者方法实现接口时,只有结构体的值可以被传递给接口,而指针接收者方法实现接口时,可以传递结构体的指针或者任何实现了该结构体指针类型的类型。这是因为在 Go 语言中,可以通过对指针类型进行间接引用来访问结构体的字段。
4.指针接收者方法还具有一些其他的优点。例如,使用指针接收者可以避免在方法中对结构体进行拷贝,从而提高程序的性能。此外,指针接收者还可以用于修改结构体中的字段值。
5.在实现接口时,应根据实际情况选择值接收者方法或指针接收者方法。如果不需要修改结构体的字段,并且希望方法能够被传递给值类型的变量,那么使用值接收者方法就可以了。如果需要修改结构体的字段,或者希望方法能够被传递给指针类型的变量,那么就应该使用指针接收者方法。
代码案例
当使用值接收者(value receiver)实现接口时,接口会拷贝值接收者的值。而当使用指针接收者(pointer receiver)实现接口时,接口会拷贝指向值接收者的指针。这两种实现方式的主要区别在于对结构体的修改是否会影响接口中对应的值。
定义了一个接口 Geometry
,并分别使用值接收者和指针接收者实现了两个结构体 Rectangle
和 Circle
:
package main
import (
"fmt"
"math"
)
type Geometry interface {
area() float64
perimeter() float64
}
type Rectangle struct {
width, height float64
}
func (r Rectangle) area() float64 {
return r.width * r.height
}
func (r Rectangle) perimeter() float64 {
return 2*r.width + 2*r.height
}
type Circle struct {
radius float64
}
func (c *Circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c *Circle) perimeter() float64 {
return 2 * math.Pi * c.radius
}
func main() {
r := Rectangle{width: 3, height: 4}
c := Circle{radius: 5}
fmt.Println("Rectangle")
fmt.Println("Area: ", r.area())
fmt.Println("Perimeter: ", r.perimeter())
fmt.Println("\nCircle")
fmt.Println("Area: ", c.area())
fmt.Println("Perimeter: ", c.perimeter())
// 值接收者实现的结构体修改不影响接口中对应的值
r.width = 6
fmt.Println("\nRectangle after modification")
fmt.Println("Area: ", r.area())
fmt.Println("Perimeter: ", r.perimeter())
// 指针接收者实现的结构体修改会影响接口中对应的值
c.radius = 7
fmt.Println("\nCircle after modification")
fmt.Println("Area: ", c.area())
fmt.Println("Perimeter: ", c.perimeter())
}
输出结果为:
Rectangle
Area: 12
Perimeter: 14
Circle
Area: 78.53981633974483
Perimeter: 31.41592653589793
Rectangle after modification
Area: 24
Perimeter: 14
Circle after modification
Area: 153.93804002589985
Perimeter: 43.982297150257104
可以看到,当使用值接收者实现 Rectangle
结构体时,结构体中的修改不会影响接口中对应的值;而使用指针接收者实现 Circle
结构体时,结构体中的修改会影响接口中对应的值。