Home>Article>Backend Development> What is the use of golang’s interface?
In golang, interface is a type that is used to wrap up methods. Its functions are: 1. As a wrapper for methods for object-oriented design; 2. As a method for various The bearer of data can be used to receive function parameters, etc. The definition syntax of the interface is "type interface type name interface{method name (parameter list 1) return value list}"; when the first letter of the method name is capitalized and the first letter of the interface type name is also capitalized, this method can be used by the package where the interface is located. (package) outside the code access.
The operating environment of this tutorial: Windows 7 system, GO version 1.18, Dell G3 computer.
Interface is a combination of method signatures. We use interface to define a set of behaviors of an object.
(Note the difference between method and ordinary func)
Interface is a type, which is different from the interface of ordinary languages. It is only used to modify the method. pack. However, it is this kind of convergence that makes the GO language have a function-based object orientation.
The main functions of the interface:
1. As a method collector for object-oriented design.
2. As a bearer of various data, it can be used to receive function parameters, etc.
This is also the case. GO language advocatesinterface-oriented programming.
2.1 Definition
Similar structure
type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 方法名2( 参数列表2 ) 返回值列表2 … }
Of course, this is only an interface definition with methods, not a data-oriented interface.
Interface name: Use type to define the interface as a custom type name. When naming Go language interfaces,eris usually added after the word. For example, the interface with writing operations is calledWriter, and the interface with string function is calledStringer##. #wait. It is best for the interface name to highlight the type meaning of the interface.
2.2 Use
As long as an object implements all the methods in the interface, then it implements the interface. In other words, an interfaceis a list of methodsthat need to be implemented.
//定义接口 type FastfoodStore interface{ MakeHamberger() MakeFriedChips() MakeSoftDrink() } //定义结构体 type KFC struct{} type HambergerKing struct{} //实现了接口中所有的方法 func (kfc KFC) MakeHamberger(){ fmt.println("肯德基的汉堡") } func (kfc KFC) MakeFriedChips(){ fmt.println("肯德基的薯条") } func (kfc KFC) MakeSoftDrink(){ fmt.println("肯德基的饮料") } func (K *HambergerKing) MakeHameberger(){ fmt.println("汉堡王的汉堡") } func (K *HambergerKing) MakeFriedChips(){ fmt.println("汉堡王的薯条") } func (K *HambergerKing) MakeSoftDrink(){ fmt.println("汉堡王的饮料") }We can see that unlike Java's explicit implementation of the interface, Go's language is implicitly implemented.
So how does the GO language check whether the type is an interface?
Answer: The Go language will only check whether a type implements the interface when passing parameters, returning parameters, and variable assignment. From the perspective of the type checking process, the compiler only checks the type when needed. When a type implements an interface, it only needs to implement all the methods in the interface, and does not need to be explicitly declared like in programming languages such as Java.We can see that when implementing the interface above, KFC is implemented using structure objects, while Hamberger king is implemented through pointers. What is the difference between the two?
Answer: The difference is when we initialize the interface//结构体初始化和指针初始化 var f faststore = KFC{} //可以通过编译 var f faststore = &KFC{} //可以通过编译 var f faststore = HambergerKing{} //无法通过编译 var f faststore = &HambergerKing{} //可以通过编译So why doesn’t it work when we use pointers to implement and initialize the structure? Answer:
The Go language uses values by value when passing parameters.
#As shown in the figure above, regardless of the variable pointer or structure initialized in the above code, value copying will occur when calling the method:如上图左侧,对于 &HambergerKing{} 来说,这意味着拷贝一个新的 &HambergerKing{} 指针,这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体;
如上图右侧,对于 HambergerKing{} 来说,这意味着方法会接受一个全新的 HambergerKing{},因为方法的参数是*HambergerKing,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体;
上面的分析解释了指针类型的现象,当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。当然这并不意味着我们应该一律使用结构体实现接口,这个问题在实际工程中也没那么重要,在这里我们只想解释现象背后的原因。
在上面我们说过,interface有两种用法,现在介绍了其中一种就是作为方法的收束器。那么第二种就是作为数据的承载者。
2.3 数据承载者
作为数据容器时,接口就是一个“空”接口,这个空来形容没有Method。空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。
需要注意的是,与 C 语言中的void *不同,interface{}类型不是任意类型。如果我们将类型转换成了 interface{} 类型,变量在运行期间的类型也会发生变化,获取变量类型时会得到 interface{}。
我们尝试从底层实现来解释两种用法的不同,你会好理解一些。Go 语言使用 runtime.iface 表示第一种接口,使用 runtime.eface 表示第二种不包含任何方法的接口 interface{},两种接口虽然都使用 interface 声明,但是由于后者在 Go 语言中很常见,所以在实现时使用了特殊的类型。
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数 func show(a interface{}) { fmt.Printf("type:%T value:%v\n", a, a) }
空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值 var studentInfo = make(map[string]interface{}) studentInfo["name"] = "Wilen" studentInfo["age"] = 18 studentInfo["married"] = false fmt.Println(studentInfo) //gin框架的gin.H{}
interface 可以存储所有的值,那么自然会涉及到类型转换这个话题。与此同时,我们也将在这节细说类型转换中,因为结构体实现和结构体指针实现的接口的异同。
3.1结构体指针实现接口
//我们仍然运用上面快餐店的例子 type Store interface{ MakeHamberger() } type KFC struct{ name string } func (k *KFC) MakeHamberger(){ fmt.println(k.name+"制作了一个汉堡") } func main(){ var s store = &KFC{name:"东街店"} store.MakeHamberger() }
这里将上述代码生成的汇编指令拆分成三部分分析:
KFC的初始化又可以分为下面几步:
获取 KFC 结构体类型指针并将其作为参数放到栈上;
通过 CALL 指定调用 runtime.newobject函数,这个函数会以 KFC 结构体类型指针作为入参,分配一片新的内存空间并将指向这片内存空间的指针返回到 SP+8 上;
SP+8 现在存储了一个指向 KFC 结构体的指针,我们将栈上的指针拷贝到寄存器 DI 上方便操作;
由于 Cat 中只包含一个字符串类型的 Name 变量,所以在这里会分别将字符串地址 &"东街店" 和字符串长度 6 设置到结构体上。
因为 KFC 结构体的定义中只包含一个字符串,而字符串在 Go 语言中总共占 16 字节,所以每一个 KFC 结构体的大小都是 16 字节。初始化 KFC 结构体之后就进入了将 *KFC 转换成 Store 类型的过程了:
类型转换的过程比较简单,Store 作为一个包含方法的接口,它在底层使用 [runtime.iface] 结构体表示。runtime.iface 结构体包含两个字段,其中一个是指向数据的指针,另一个是表示接口和结构体关系的 tab 字段,我们已经通过上一段代码 SP+8 初始化了 KFC 结构体指针,这段代码只是将编译期间生成的 runtime.itab 结构体指针复制到 SP 上:
到这里,我们会发现 SP ~ SP+16 共同组成了 runtime.iface 结构体。
栈上的这个 runtime.iface 也是 MakeHamberger() 方法的第一个入参。通过CALL()完成方法的调用。
3.2 结构体实现接口
//我们仍然运用上面快餐店的例子 type Store interface{ MakeHamberger() } type KFC struct{ name string } func (k KFC) MakeHamberger(){ fmt.println(k.name+"制作了一个汉堡") } func main(){ var s store = KFC{name:"东街店"} store.MakeHamberger() }
如果我们在初始化变量时使用指针类型 &KFC{Name: "东街店"} 也能够通过编译,不过生成的汇编代码和上一节中的几乎完全相同,所以这里也就不分析这个情况了。
在栈上初始化 KFC 结构体,而上一节的代码在堆上申请了 16 字节的内存空间,栈上只有一个指向 KFC 的指针。
初始化结构体后会进入类型转换的阶段,编译器会将 go.itab."".KFC,"".Store 的地址和指向 KFC 结构体的指针作为参数一并传入 runtime.convT2I 函数:这个函数会获取 runtime.itab 中存储的类型,根据类型的大小申请一片内存空间并将 elem 指针中的内容拷贝到目标的内存中:
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) { t := tab._type x := mallocgc(t.size, t, true) typedmemmove(t, x, elem) i.tab = tab i.data = x return }
runtime.convT2I 会返回一个 runtime.iface,其中包含 runtime.itab 指针和 KFC 变量。当前函数返回之后,main 函数的栈上会包含以下数据:
SP 和 SP+8 中存储的 runtime.itab 和 KFC 指针是 runtime.convT2I 函数的入参,这个函数的返回值位于 SP+16,是一个占 16 字节内存空间的 runtime.iface 结构体,SP+32 存储的是在栈上的 KFC 结构体,它会在 runtime.convT2I 执行的过程中拷贝到堆上。
3.3类型断言
如何将一个接口类型转换成具体类型?
x.(T)
func main() { var c Store = &KFC{Name: "东街店"} switch c.(type) { case *KFC: kfc := c.(*KFC) kfc.MakeHamberger() } }
因为 Go 语言的编译器做了一些优化,所以代码中没有runtime.iface 的构建过程,不过对于这一节要介绍的类型断言和转换没有太多的影响。
switch语句生成的汇编指令会将目标类型的 hash 与接口变量中的 itab.hash 进行比较
func main() { var c interface{} = &KFC{Name: "东街店"} switch c.(type) { case *KFC: kfc := c.(*KFC) kfc.MakeHamberger() } }
上述代码会在类型断言时就不是直接获取变量中具体类型的 runtime._type,而是从 eface._type 中获取,汇编指令仍然会使用目标类型的 hash 与变量的类型比较.
The above is the detailed content of What is the use of golang’s interface?. For more information, please follow other related articles on the PHP Chinese website!