Go语言的接口类型interface

文章详细介绍了Go语言中的接口类型,包括接口的定义、使用,类型断言的操作,以及接口变量的底层原理。强调了尽量使用小接口的设计原则,并通过示例展示了接口变量赋值(装箱)的过程。此外,文章还对比了nil、空接口和非空接口的等值比较情况。
摘要由CSDN通过智能技术生成

目录

什么是接口类型

类型断言(Type Assertion)

尽量使用小接口

接口类型变量的底层原理

对比nil、空接口、非空接口

(1)nil 接口变量

(2)空接口类型变量

(3)非空接口类型变量

(4)空接口类型变量与非空接口类型变量的等值比较

装箱:接口类型变量赋值的过程


什么是接口类型

接口类型是由 type 和 interface 关键字定义的一组方法集合,Go 语言中的接口类型与它的实现者之间的关系不需要像其他语言(比如 Java / PHP)使用 implements 关键词修饰,实现者只需要实现接口方法中的全部方法即可(鸭子类型)。接口的使用方法如下代码所示:

// go02/interface.go
package main

// 定义一个非空接口
type UserInterface interface {
	getUserName(uid int) (string, error) //根据uid获取用户名的方法定义,形参uid可省略
	getUserInfo(username string) string  //根据用户名获取用户详细信息的方法定义,形参username可省略
}

// 接口的实现:定义结构体和方法
type users struct {
	age  int
	name string
}
func (u *users) getUserName(uid int) (string, error) {
	return "张三", nil
}
func (u *users) getUserInfo(username string) string {
	return fmt.Sprintf("用户名是:%v", username)
}

// 定义一个空接口, 等价于 interface{}
type EmptyInterface interface {
}

// 接口被定义后,就可以用于声明变量
var ui UserInterface // ui 是一个 UserInterface 接口类型的实例变量,也叫做:接口类型变量,零值是 nil

func main() {
	var u = users{}
	fmt.Println(u.getUserName(1))      //张三 <nil>
	fmt.Println(u.getUserInfo("haha")) //用户名是:haha
}

接口类型变量的零值是 nil;由于空接口类型的方法集合为空,所以可以将任何类型的值赋值给空接口:

var i interface{} = 15 // ok
i = "hello, golang" // ok
type T struct{}
var t T
i = t // ok
i = &t // ok

Go语言中接口的特点:

  • Go语言中的接口类型与其他数据类型不同,它是没法被实例化的。既不能调用 new 函数或 make 函数创建出一个接口类型的值,也无法用字面量来表示一个接口类型的值。 
  • 对于某一个接口类型来说,如果没有任何数据类型可以作为它的实现,那么该接口的值就不存在。

类型断言(Type Assertion)

通过一个接口类型变量还原它的右值的类型与值信息,基本语法: v, ok := i.(T) ,也可以省略ok,但有可能出现panic,因此不建议省略。

package main

import "fmt"

type myInterface interface {
	method1()
}
type myInt int

func (myInt) method1() {
	println("myInt 的方法 method1")
}
func main() {
	//类型断言(Type Assertion):通过一个接口类型变量还原它的右值的类型与值信息
	var a int64 = 20
	var i interface{} = a
	v1, ok := i.(int64)
	fmt.Printf("v1:%d, type:%T, ok:%t \n", v1, v1, ok) // v1:20, type:int64, ok:true
	v2 := i.(int64)                                    // 省略ok
	fmt.Printf("v2:%d, type:%T\n", v2, v2)             // v2:20, type:int64

	v3, ok := i.(string)
	fmt.Printf("v3:%s, type:%T, ok:%t \n", v3, v3, ok) // v3:, type:string, ok:false
	// v4 := i.(string)                                // 省略ok,导致 panic: interface conversion: interface {} is int64, not string

	var myi myInt
	var myj interface{} = myi
	v5, ok := myj.(myInterface)
	fmt.Printf("v5: type:%T, ok:%t \n", v5, ok) // v5: type:<nil>, ok:false
	v5.method1()                                // myInt 的方法 method1
	fmt.Printf("v5: type:%T \n", v5)            // v5: type:main.myInt
}

接口类型的类型断言还有一种方式:switch type

package main
 
import "fmt"
 
func main() {
	x := "hello"
	fmt.Println(getVariableType(x)) //the type of v is string, v = hello
}
 
func getVariableType(x interface{}) string {
	switch v := x.(type) { //此处的 v 存储的是变量 x 的动态类型对应的值信息
	case nil:
		return "v is nil"
	case int:
		return fmt.Sprintf("the type of v is int, v = %v", v)
	case string:
		return fmt.Sprintf("the type of v is string, v = %v", v)
	case bool:
		return fmt.Sprintf("the type of v is bool, v = %v", v)
	default:
		return fmt.Sprintf("don't support the type")
	}
}

尽量使用小接口

尽量定义小接口,即方法个数在 1~3 个之间的接口。比如标准库中如下常用的接口定义:

// $GOROOT/src/builtin/builtin.go
type error interface {
	Error() string
}
// $GOROOT/src/io/io.go
type Reader interface {
	Read(p []byte) (n int, err error)
}
// $GOROOT/src/net/http/server.go
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
type ResponseWriter interface {
	Header() Header
	Write([]byte) (int, error)
	WriteHeader(int)
}

小接口有以下优势:

  • 接口越小,抽象程度越高,对应的集合空间就越大。比如空接口 interface{} 包含了 Go 语言世界的所有事物;
  • 小接口易于实现和测试,因为方法比较少,在单元测试的时候有优势;
  • 小接口职责单一,易于复用组合,可以将已有接口组合成新的接口,比如标准库 io.ReadWriter
// $GOROOT/src/io/io.go

type ReadWriter interface {
	Reader
	Writer
}

type Reader interface {
	Read(p []byte) (n int, err error)
}
type Writer interface {
	Write(p []byte) (n int, err error)
}

定义接口的原则:别管接口大小,先抽象出接口,一开始可以先不考虑接口的数量,等到后期再将大接口拆分为小接口。就像数据库一样,刚开始先不要考虑分表,先把业务实现了,等到数据量达到一定级别的时候再考虑分表。

接口类型变量的底层原理

接口类型变量的内部表示和静态类型变量(如int、float64)是不一样的,可以在标准库 runtime2.go 中看到,在运行时层面,接口类型变量有两种内部表示:iface和eface,iface 用于表示拥有方法的接口 interface 类型变量,eface 用于表示没有方法的空接口类型变量,也就是 interface{}类型的变量。具体的底层代码如下:

// $GOROOT/src/runtime/runtime2.go
//iface 用于表示拥有方法的接口 interface 类型变量
type iface struct {
	tab *itab //iface 除了要存储动态类型信息之外,还要存储接口本身的信息以及动态类型所实现的方法的信息
	data unsafe.Pointer //指向当前赋值给该接口类型变量的动态类型变量的值
}

//eface 用于表示没有方法的空接口类型变量,也就是 interface{}类型的变量
type eface struct {
	_type *_type //eface 表示的空接口类型并没有方法列表
	data unsafe.Pointer
}

// $GOROOT/src/runtime/runtime2.go
type itab struct {
	inter *interfacetype //存储这个接口类型自身的信息
	_type *_type //存储这个接口类型变量的动态类型的信息
	hash uint32 // copy of _type.hash. Used for type switches.
	_ [4]byte
	fun [1]uintptr //动态类型已实现的接口方法的调用地址数组
}

// $GOROOT/src/runtime/type.go
type interfacetype struct {
	typ _type //类型信息(typ)
	pkgpath name //包路径名(pkgpath)
	mhdr []imethod //接口方法集合切片(mhdr)
}

// $GOROOT/src/runtime/type.go
type _type struct {
	size uintptr
	ptrdata uintptr // size of memory prefix holding all pointers
	hash uint32
	tflag tflag
	align uint8
	fieldAlign uint8
	kind uint8
	equal func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata *byte
	str nameOff
	ptrToThis typeOff
}

Go 语言中每种类型都会有唯一的 _type 信息,无论是内置原生类型还是自定义类型都有。Go 运行时会为程序内的全部类型建立只读的共享 _type 信息表,因此拥有相同动态类型的同类接口类型变量的 _type/tab 信息是相同的。判断两个接口类型变量是否相同,只需要判断 _type/tab 是否相同,以及data 指针指向的内存空间所存储的数据值是否相同就可以了。

可以使用一些 helper 函数输出接口类型变量的内部表示,可以很方便的看出两个变量是否相等,使用预定义函数 println 可以输出 eface 或 iface 的两个指针字段的值。针对 eface 和 iface 类型的打印函数实现如下:

// $GOROOT/src/runtime/print.go
// printeface 和 printiface 会输出各自的两个指针字段的值
func printeface(e eface) {
	print("(", e._type, ",", e.data, ")")
}
func printiface(i iface) {
	print("(", i.tab, ",", i.data, ")")
}

对比nil、空接口、非空接口

(1)nil 接口变量

未赋初值的接口类型变量的值为 nil,这类变量也就是 nil 接口变量。无论是空接口类型还是非空接口类型变量,一旦变量值为 nil,那么它们内部表示均为(0x0,0x0),也就是类型信息、数据值信息均为空。因此下面的变量 i 和 err 等值判断为 true:

func printNilInterface() {
	var i interface{}   // 空接口类型
	var err error       // 非空接口类型
	println(i)          // (0x0,0x0)
	println(err)        // (0x0,0x0)
	println(i == nil)   // true
	println(err == nil) // true
	println(i == err)   // true
}

(2)空接口类型变量

对于空接口类型变量,只有 _type 和 data 所指数据内容一致的情况下,两个空接口类型变量之间才能划等号。

func printEmptyInterface() {
	var eif1 interface{} // 空接口类型
	var eif2 interface{} // 空接口类型

	//eif1 与 eif2 已经分别被赋值整型值 17 与 18,它们的动态类型信息是相同的(都是 0x1097200),但data指针指向的内存值不同,因此不相等
	eif1 = 17
	eif2 = 18
	println("eif1:", eif1)                //eif1: (0x1097200,0x10c7fd0)
	println("eif2:", eif2)                //eif2: (0x1097200,0x10c7fd8)
	println(eif1 == eif2) // false

	//eif2 重新赋值为 17,此时 eif1 和 eif2 不仅存储的动态类型相同(都是 0x1097200),而且data 指针指向的内存值也相同了,因此相等
	eif2 = 17
	println("eif1:", eif1)                //eif1: (0x1097200,0x10c7fd0)
	println("eif2:", eif2)                //eif2: (0x1097200,0x10c7fd0)
	println(eif1 == eif2) // true

	//eif2 重新赋值为 int64 类型的数值17,此时 eif1 和 eif2 存储的动态类型信息不同,就算值相同,依然是不相等的
	eif2 = int64(17)
	println("eif1:", eif1)                //eif1: (0x1097200,0x10c7fd0)
	println("eif2:", eif2)                //eif2: (0x10972c0,0x10c7fd0)
	println(eif1 == eif2) // false
}

(3)非空接口类型变量

对于非空接口类型变量,只有 tab 和 data 指的数据内容一致的情况下,两个非空接口类型变量之间才能划等号。

type T int

func (t T) Error() string {
	return "bad error"
}
func printNonEmptyInterface() {
	var err1 error // 非空接口类型
	var err2 error // 非空接口类型
	err1 = (*T)(nil)
	println("err1:", err1) //err1: (0x10ca8a8,0x0)
	println(err1 == nil)   //false
	err1 = T(5)
	err2 = T(6)
	println("err1:", err1) //err1: (0x10ca888,0x10ca268)
	println("err2:", err2) //err2: (0x10ca888,0x10ca270)
	println(err1 == err2)  //false
	err2 = fmt.Errorf("%d\n", 5)
	println("err1:", err1) //err1: (0x10ca888,0x10ca268)
	println("err2:", err2) //err2: (0x10ca768,0xc00010a230)
	println(err1 == err2)  //false
}

(4)空接口类型变量与非空接口类型变量的等值比较

空接口类型变量和非空接口类型变量内部表示的结构有所不同(第一个字段:_type vs. tab),Go 在进行等值比较时,类型比较使用的是 eface 的 _type 和 iface 的 tab._type。下面例子中 当 eif
和 err 都被赋值为T(5)时,两者之间是划等号的。

func printEmptyInterfaceAndNonEmptyInterface() {
	var eif interface{} = T(5)
	var err error = T(5)
	println("eif:", eif) //eif: (0x109b360,0x10ca400)
	println("err:", err) //err: (0x10caa18,0x10ca400)
	println(eif == err)  //true
	err = T(6)
	println("eif:", eif) //eif: (0x109b360,0x10ca400)
	println("err:", err) //err: (0x10caa18,0x10ca408)
	println(eif == err)  //false
}

装箱:接口类型变量赋值的过程

实接口类型变量赋值是一个“装箱”(boxing)的过程。装箱一般是指把一个值类型转换成引用类型,比如在 Java 语言中将一个 int 变量转换成 Integer 对象就是一个装箱操作。在 Go 语言中,将任意类型赋值给一个接口类型变量就是装箱操作,接口类型的装箱实际就是创建一个 eface 或 iface 的过程。经过装箱后,新分配的内存空间中的数据与原变量就没有关系了。

// go02/interface2.go
package main

import "fmt"

type users struct {
	age  int
	name string
}

func (users) M1() {}
func (users) M2() {}

type EmptyInterface interface {
}

type NonEmptyInterface interface {
	M1()
	M2()
}

func main() {
	var u = users{
		age:  21,
		name: "rxbook",
	}
	fmt.Println(u) //{21 rxbook}

	//开始装箱,经过装箱后,新分配的内存空间中的数据与原变量就没有关系了
	var i interface{} = u       //赋给空接口
	var j EmptyInterface = u    //赋给空接口(另一种写法)
	var k NonEmptyInterface = u //赋给非空接口
	u.age = 28
	fmt.Println(u) //修改原变量值后: {28 rxbook}

	//以下箱内数据都不会变
	fmt.Println(i) //{21 rxbook}
	fmt.Println(j) //{21 rxbook}
	fmt.Println(k) //{21 rxbook}

	//再比如下面这个精简的例子
	var x int = 61
	var y interface{} = x
	x = 62         // x的值已经改变
	fmt.Println(y) // y的值仍是61
}

源代码:https://gitee.com/rxbook/go-demo-2023/tree/master/basic/go02

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浮尘笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值