12 go语言(golang) - 数据类型:接口
接口
在 Go 语言中,接口(interface)是一种抽象类型,它定义了一组方法的集合,但不包含这些方法的实现。是 Go 语言实现多态和面向对象编程的重要机制之一。
1、接口定义
在 Go 中,接口通过 type
关键字进行定义,并且包含一组方法签名。
type JDBCUtil interface {
GetConnection() string
CloseConnection(connection string) string
}
在上面的例子中,JDBCUtil
是一个接口,它要求任何实现该接口的类型都必须有 GetConnection()
和 CloseConnection(connection string)
方法。
2、实现接口
Go 中不需要显式地声明某个类型实现了某个接口,只要该类型提供了与接口所需的方法相同的方法集即可。这被称为隐式实现。
type MysqlUtil struct {
url string
}
func (m MysqlUtil) GetConnection() string {
return "模拟获取连接:" + m.url
}
func (_ MysqlUtil) CloseConnection(connection string) string {
return fmt.Sprintf("成功关闭连接:【%s】", connection)
}
在这个例子中,MysqlUtil
类型隐式地实现了 JDBCUtil
接口,因为它具有与 JDBCUtil
接口相匹配的方法集。
3、使用接口
可以将具体类型赋值给一个接口变量,从而通过该变量调用其方法。
func Test1(t *testing.T) {
util := MysqlUtil{"jdbc://xxx..."}
connection := util.GetConnection()
fmt.Println(connection)
status := util.CloseConnection(connection)
fmt.Println(status)
}
输出:
模拟获取连接:jdbc://xxx...
成功关闭连接:【模拟获取连接:jdbc://xxx...】
4、空接口
空接口(interface{})是特殊的,它没有任何方法,因此所有类型都默认实现空接口。这使得空接口非常适合用于存储任意数据。
func Test2(t *testing.T) {
var i interface{}
i = "abc"
fmt.Printf("值:%v,类型:%T \n", i, i)
i = 123
fmt.Printf("值:%v,类型:%T \n", i, i)
}
type AnyType interface {
}
func Test3(t *testing.T) {
// 推荐简写为:list := []interface{}{1, "a", 3.14, true} ,这样就不用定义AnyType了
var list = []AnyType{1, "a", 3.14, true}
for _, value := range list {
fmt.Printf("值:%v,类型:%T \n", value, value)
}
}
输出:
=== RUN Test2
值:abc,类型:string
值:123,类型:int
=== RUN Test3
值:1,类型:int
值:a,类型:string
值:3.14,类型:float64
值:true,类型:bool
5、类型断言
当使用空或通用性较强的接口时,你可能需要知道具体存储的数据是什么,这时可以使用类型断言来处理。
func Test4(t *testing.T) {
var util = MysqlUtil{"jdbc://xxx..."}
var i interface{} = util
if _, ok := i.(JDBCUtil); ok {
fmt.Println("JDBCUtil 实现了接口:MysqlUtil")
} else {
fmt.Println("JDBCUtil 没有实现接口:MysqlUtil")
}
}
func Test5(t *testing.T) {
var i interface{} = MysqlUtil{}
switch i.(type) {
case string:
println("类型是string")
case int:
println("类型是int")
default:
println("未知类型")
}
}
输出:
=== RUN Test4
JDBCUtil 实现了接口:MysqlUtil
=== RUN Test5
未知类型
6、多态
多态(Polymorphism)在计算机科学中指的是一种能力,即相同的操作可以作用于不同的数据类型上。在面向对象编程中,多态性通常意味着一个接口可以被多个类型实现,而这些类型可以在不修改调用代码的情况下被使用。
在 Go 语言中,多态性主要通过接口来实现。
多态性的优点
-
灵活性和可扩展性:你只需确保新添加的类型符合某个既定行为(即,实现了特定的方法集),就能将其无缝地融入现有系统。
-
代码重用和简化:通过抽象出公共行为,可以避免重复代码,并使得代码更易于维护和理解。
-
解耦合设计:调用者无需知道具体对象是什么,只需关心它们是否符合所需行为。这种解耦合设计使得系统模块之间更加独立。
练习
在项目中实现注册成功之后向用户发送:邮件、手机的消息提醒。
// 首先,定义一个接口,它包含需要实现的方法。这些方法代表了某种行为或功能。
type Message interface {
SendMsg() string
}
// 不同的具体类型可以通过实现该接口的方法来表现不同的行为。
type Phone struct {
phoneNumber int
}
type Email struct {
email string
}
func (a Phone) SendMsg() string {
fmt.Printf("%d,手机用户注册成功\n", a.phoneNumber)
return "success"
}
func (e Email) SendMsg() string {
fmt.Printf("%s,邮箱用户注册成功\n", e.email)
return "success"
}
// 基于接口的参数,可以实现传入多中类型(多态),也同时具有约束对象必须实现接口方法的功能
func sendMsg(message Message) {
message.SendMsg()
}
func Test6(t *testing.T) {
email := Email{"xxxx@qq.com"}
phone := Phone{15800000000}
sendMsg(email)
sendMsg(phone)
}
输出:
xxxx@qq.com,邮箱用户注册成功
15800000000,手机用户注册成功
底层原理
在 Go 语言中,接口的底层实现是通过一种称为“接口表”(interface table)的机制来实现的。这种机制使得接口能够动态地绑定到具体类型,并支持多态性。
1、空接口的内部结构
type eface struct {
_type *_type // 存储类型相关信息
data unsafe.Pointer // 存储数据
}
- 类型信息(type information):指向一个描述具体类型的结构体,这个结构体包含了该类型的方法集和其他元数据。
- 数据指针(data pointer):指向实际存储的数据。如果这个数据是一个值类型,则直接存储;如果是一个引用或指针,则存储的是地址。
2、非空接口的内部结构
type iface struct {
tab *itab // 指向类型表,它包含了具体类型如何实现该接口的方法信息
data unsafe.Pointer // 指向实际数据的指针
}
type ITab struct {
Inter *InterfaceType // 接口信息,如:接口中定义的方法。
Type *Type
Hash uint32
Fun [1]uintptr
}
type InterfaceType struct {
Type
PkgPath Name
Methods []Imethod // 接口的方法
}
- tab(*itab):这是一个指向
itab
的指针。itab
是一种描述特定类型如何实现某个特定接口的数据结构。它包括了关于该具体类型的方法偏移等信息,以便在运行时能够正确地进行方法调用。是连接特定接口与其实现之间的桥梁。 - data(unsafe.Pointer):与
eface
中类似,这是一个不安全指针,用于存储实际的数据地址。如果是值类型,则直接存储其地址;如果是引用或指针,则存储的是该引用或指针所指向内容的地址。 - interfacetype:描述一个 Go 接口本身,包括其所属包和需要的方法集。
3、空接口赋值过程
如果在代码中出现其他对象
赋值给空接口,其实就是将其他对象相关的值存放到eface的 _type
和data
中,内部源码:
// The conv and assert functions below do very similar things.
// The convXXX functions are guaranteed by the compiler to succeed.
// The assertXXX functions may fail (either panicking or returning false,
// depending on whether they are 1-result or 2-result).
// The convXXX functions succeed on a nil input, whereas the assertXXX
// functions fail on a nil input.
// convT converts a value of type t, which is pointed to by v, to a pointer that can
// be used as the second word of an interface value.
func convT(t *_type, v unsafe.Pointer) unsafe.Pointer {
if raceenabled {
raceReadObjectPC(t, v, getcallerpc(), abi.FuncPCABIInternal(convT))
}
if msanenabled {
msanread(v, t.Size_)
}
if asanenabled {
asanread(v, t.Size_)
}
// 使用 mallocgc 为目标类型分配足够大小的堆空间。这个函数会根据传入参数决定是否进行垃圾回收标记。
x := mallocgc(t.Size_, t, true)
// 使用 typedmemmove 将源地址中的内容复制到新分配的位置。这一步确保了原始数据被正确地移动到新的位置,并且新位置符合目标类型的信息。
typedmemmove(t, x, v)
// 返回新分配空间的地址,这个地址可以作为接口值中的数据部分使用。
return x
}
当你执行类似以下代码时:
var i interface{}
i = someValue
Go 会进行如下操作:
- 获取动态类型:查找并获取
someValue
的动态_type
信息,这包括其内存布局、方法集等元数据信息。 - 设置 eface:
- 将
_type*
, 即动态类型信息,保存到eface._type
- 将实际数据地址保存到
eface.data
- 将
思考
1、接口没有强制性的要求我们实现它的方法,跟java不同,那么它的约束性就小了很多,这种情况下还需要使用接口吗?或者说接口的意义是否低了很多?
Go 的接口实现是隐式的,这与 Java 等语言中的显式实现不同,Go 语言的设计哲学之一是“显式优于隐式”,这意味着 Go 语言倾向于让开发者明确地知道他们正在做什么,而不是依赖于编译器来推断或强制实现某些行为。
接口的隐式实现
在 Go 中,类型不需要显式声明实现某个接口。只要类型实现了接口的所有方法,它就自动实现了该接口。这种隐式实现方式减少了样板代码,使得代码更加简洁。
接口的灵活性
由于接口的隐式实现,Go 语言中的接口非常灵活。你可以在不修改现有类型的情况下,通过实现新的接口来扩展类型的功能。这种灵活性使得 Go 语言在处理多种不同类型的场景时非常强大。
接口的多态性
尽管 Go 语言没有类和继承的概念,但接口提供了一种实现多态的方式。你可以定义一个函数,它接受一个接口类型的参数,然后传递任何实现了该接口的类型。这使得你可以编写更加通用和可重用的代码。
2、在JAVA中,当我们实现某个接口时,idea会提示我们具体需要实现的所有方法,而golang中没有,那么是不是我们就很容易漏掉需要实现的方法
文档和注释
在定义接口时,可以在文档中详细列出所有方法。这样,当你实现接口时,可以参考这些文档来确保没有遗漏任何方法。
代码审查
代码审查是一个很好的实践,可以帮助发现遗漏的方法实现。同事或团队成员可以检查你的代码,确保你已经实现了接口的所有方法。
测试
编写单元测试来测试你的类型是否正确实现了接口。这不仅可以帮助你发现遗漏的方法,还可以确保你的实现按预期工作。
IDE 插件和工具
使用支持 Go 语言的 IDE(如 GoLand)或集成开发环境(如 Visual Studio Code)时,可以使用一些插件和工具来帮助你识别未实现的接口方法。例如,GoLand 有一个功能,可以在你实现接口时自动提示你需要实现的方法。