代码必须能够被人阅读,只是机器恰好可以执行
Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。
但是 Go 语言里有非常灵活的 接口
概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
不像大多数面向对象编程语言,在 Go 语言中接口可以有值,一个接口类型的变量或一个 接口值 :var ai Namer
,ai 是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针,虽然不完全是一回事。指向接口值的指针是非法的,它们不仅一点用也没有,还会导致代码错误。
此处的方法指针表是通过运行时反射能力构建的。
类型(比如结构体)可以实现某个接口的方法集;这个实现可以描述为,该类型的变量上的每一个具体方法所组成的集合,包含了该接口的方法集。
实现了 Namer 接口的类型的变量可以赋值给 ai(即 receiver 的值),方法表指针(method table ptr)就指向了当前的方法实现。当另一个实现了 Namer 接口的类型的变量被赋给 ai,receiver 的值和方法表指针也会相应改变。
很多面向对象语言都有接口这一概念,例如 Java 和 C#。Java 的接口不仅可以定义方法签名,还可以定义变量,这些定义的变量可以直接在实现接口的类中使用:
public interface MyInterface {
String hello = "Hello";
public void sayHello();
}
上述代码定义了一个必须实现的方法 sayHello
和一个会注入到实现类的变量 hello
。在下面的代码中,MyInterfaceImpl
就实现了 MyInterface
接口:
public class MyInterfaceImpl implements MyInterface {
public void sayHello() {
System.out.println(MyInterface.hello);
}
}
Java
中的类必须通过上述方式显式地声明实现的接口,但是在 Go
语言中实现接口就不需要使用类似的方式。首先,我们简单了解一下在 Go
语言中如何定义接口。定义接口需要使用 interface
关键字,在接口中我们只能定义方法签名,不能包含成员变量,一个常见的 Go
语言接口是这样的:
type error interface {
Error() string
}
如果一个类型需要实现 error
接口,那么它只需要实现 Error() string
方法,下面的 RPCError
结构体就是 error
接口的一个实现:
type RPCError struct {
Code int64
Message string
}
func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}
细心的读者可能会发现上述代码根本就没有 error 接口的影子,这是为什么呢?Go 语言中接口的实现都是隐式的,我们只需要实现 Error() string 方法实现了 error 接口。Go 语言实现接口的方式与 Java 完全不同:
我们使用上述 RPCError 结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查,这里举几个例子来演示发生接口类型检查的时机:
func main() {
var rpcErr error = NewRPCError(400, "unknown err") // typecheck1
err := AsErr(rpcErr) // typecheck2
println(err)
}
func NewRPCError(code int64, msg string) error {
return &RPCError{ // typecheck3
Code: code,
Message: msg,
}
}
func AsErr(err error) error {
return err
}
Go 语言会编译期间对代码进行类型检查,上述代码总共触发了三次类型检查:
接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}:
Go 语言使用 iface 结构体表示第一种接口,使用 eface 结构体表示第二种空接口,两种接口虽然都使用 interface 声明,但是由于后者在 Go 语言中非常常见,所以在实现时使用了特殊的类型。
需要注意的是,与 C 语言中的 void 不同,interface{} 类型不是任意类型,如果我们将类型转换成了 interface{} 类型,这边变量在运行期间的类型也发生了变化,获取变量类型时就会得到 interface{} 。
package main
func main() {
type Test struct{}
v := Test{}
Print(v)
}
func Print(v interface{}) {
println(v)
}
上述函数不接受任意类型的参数,只接受 interface{} 类型的值,在调用 Print 函数时会对参数 v 进行类型转换,将原来的 Test 类型转换成 interface{} 类型。
Go 语言根据接口类型『是否包含一组方法』
对类型做了不同的处理。我们使用 iface
结构体表示包含方法的接口;使用 eface
结构体表示不包含任何方法的 interface{}
类型,eface
结构体在 Go
语言的定义是这样的:
type eface struct { // 16 bytes
_type *_type
data unsafe.Pointer
}
由于 interface{}
类型不包含任何方法,所以它的结构也相对来说比较简单,只包含指向底层数据和类型的两个指针。从上述结构我们也能推断出 Go
语言中的任意类型都可以转换成 interface{}
类型。
另一个用于表示接口的结构体就是 iface
,这个结构体中有指向原始数据的指针 data
,不过更重要的是 itab
类型中的 tab
字段。
type iface struct { // 16 bytes
tab *itab
data unsafe.Pointer
}
、
接下来我们将详细分析 Go 语言接口中的这两个类型,即 _type 和 itab
。
_type
是 Go
语言类型的运行时表示。下面是运行时包中的结构体,结构体包含了很多元信息,例如:类型的大小、哈希、对齐以及种类等。
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
我们只需要对 _type 结构体中的字段有一个大体的概念,不需要详细理解所有字段的作用和意义。
itab 结构体是接口类型的核心组成部分,每一个 itab 都占 32 字节的空间,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示:
type itab struct { // 32 bytes
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr
}
除了 inter 和 _type 两个用于表示类型的字段之外,上述结构体中的另外两个字段也有自己的作用:
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。
比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法。
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
通过接口嵌套接口这种组合代替继承的方式,可以间接实现接口的多重继承功能。
在 Go 语言中同时使用指针和接口时会发生一些让人困惑的问题,接口在定义一组方法时没有对实现的接收者做限制,所以我们会看到『一个类型』
实现接口的两种方式:
这是因为结构体类型和指针类型是完全不同的,就像我们不能向一个接受指针的函数传递结构体,在实现接口时这两种类型也不能划等号。
上图中的两种实现不可以同时存在,Go 语言的编译器会在结构体类型和指针类型都实现一个方法时报错: method redeclared
。
对 Cat 结构体来说,它在实现接口时可以选择接受者的类型,即结构体或者结构体指针,在初始化时也可以初始化成结构体或者指针。下
面的代码总结了如何使用结构体、结构体指针实现接口,以及如何使用结构体、结构体指针初始化变量。
type Cat struct {}
type Duck interface { ... }
type (c Cat) Quack {} // 使用结构体实现接口
type (c *Cat) Quack {} // 使用结构体指针实现接口
var d Duck = Cat{} // 使用结构体初始化变量
var d Duck = &Cat{} // 使用结构体指针初始化变量
实现接口的类型和初始化返回的类型两个维度组成了四种情况,这四种情况并不都能通过编译器的检查:
四种中只有『使用指针实现接口,使用结构体初始化变量』
无法通过编译,其他的三种情况都可以正常执行。当实现接口的类型和初始化变量时返回的类型时相同时,代码通过编译是理所应当的:
而剩下的两种方式为什么一种能够通过编译,另一种无法通过编译呢?我们先来看一下能够通过编译的情况,也就是方法的接受者是结构体,而初始化的变量是结构体指针:
type Cat struct{}
func (c Cat) Quack() {
fmt.Println("meow")
}
func main() {
var c Duck = &Cat{}
c.Quack()
}
作为指针的 &Cat{} 变量能够隐式地获取到指向的结构体,所以能在结构体上调用 Walk 和 Quack 方法。我们可以将这里的调用理解成 C 语言中的 d->Walk() 和 d->Speak(),它们都会先获取指向的结构体再执行对应的方法。
但是如果我们将上述代码中方法的接受者和初始化的类型进行交换,代码就无法通过编译了:
type Duck interface {
Quack()
}
type Cat struct{}
func (c *Cat) Quack() {
fmt.Println("meow")
}
func main() {
var c Duck = Cat{}
c.Quack()
}
$ go build interface.go
./interface.go:20:6: cannot use Cat literal (type Cat) as type Duck in assignment:
Cat does not implement Duck (Quack method has pointer receiver)
编译器会提醒我们:Cat 类型没有实现 Duck 接口,Quack 方法的接受者是指针。这两个报错对于刚刚接触 Go 语言的开发者比较难以理解,如果我们想要搞清楚这个问题,首先要知道 Go 语言在传递参数时都是传值的。
如上图所示,无论上述代码中初始化的变量 c 是 Cat{} 还是 &Cat{},使用 c.Quack() 调用方法时都会发生值拷贝:
上面的分析解释了指针类型的现象,当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;
当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。
先来看只有结构体的情况:
func(receiver Type) Method
func(receiver *Type) Method
type Person struct{
Name string
Age int
}
// 值方法
func(p Person) SayHello(){
fmt.Printf("Hello, my name is %s\n", p.Name)
}
// 引用方法
func(p *Person) SetAge(age int){
p.Age = age
}
func main(){
var jack = Person{"jack", 10}
jack.SayHello()
jack.SetAge(20)
fmt.Println(jack.Age) // 20
}
很明显,值类型肯定可以调用值方法,而对于指针方法的调用,其实是golang
的语法糖,调用jack.SetAge
的时候,会自动转换成(&jack).SetAge
type Person struct{
Name string
Age int
}
// 值方法
func(p Person) SayHello(){
fmt.Printf("Hello, my name is %s\n", p.Name)
}
// 引用方法
func(p *Person) SetAge(age int){
p.Age = age
}
func main(){
var jack = &Person{"jack", 10}
jack.SayHello()
jack.SetAge(20)
fmt.Println(jack.Age) // 20
}
指针对象调用值方法也是golang
中的语法糖,在调用值方法jack.SayHello
的时候,会自动转换成(*jack).SayHello
如果我们调用一个接口里面的函数,结构体对象实现接口时的方法可能是指针方法也可以是值方法,那么需要注意:
package main
import (
"fmt"
)
// 接口类型
type Human interface{
SayHello()
SetAge(age int)
GetAge()int
}
// 结构体对象
type Person struct{
Name string
Age int
}
// 值方法
func(p Person) SayHello(){
fmt.Printf("Hello, my name is %s\n", p.Name)
}
// 指针方法
func(p *Person) SetAge(age int){
p.Age = age
}
// 值方法
func(p Person)GetAge()int{
return p.Age
}
func main(){
// jack是个接口,被一个指针对象赋值,下面的方法都可以正确执行
var jack Human // 声明一个接口类型的对象
jack = &Person{"jack", 10} // Person实现了接口
jack.SayHello()
jack.SetAge(20)
fmt.Println(jack.GetAge())
// 值类型并没有实现SetAge的方法,所以赋值的时候会报错
// cannot use Person literal (type Person) as type Human in assignment:
// Person does not implement Human (SetAge method has pointer receiver)
var Tom Human
Tom = Person{"Tom", 12}
Tom.SayHello()
Tom.SetAge(10)
fmt.Println(Tom.GetAge())
}
以指针类型接收者实现接口的时候,只有对应的指针类型才被认为实现了该接口。
小结:
那么什么时候使用指针方法,什么时候使用值方法呢,可以考虑:
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://cjdhy.blog.csdn.net/article/details/126332709
内容来源于网络,如有侵权,请联系作者删除!