go数据类型-空结构体、空接口、nil
空结构体
func main() {
a := struct{}{}
fmt.Println(unsafe.Sizeof(a))
fmt.Printf("%p\n", &a)
}
打印
0
0x117f4e0
有经验的开发人员都知道,所有的空结构体是指向一个 zerobase的地址,而且大小为0
一般用来作结合map作为set 或者 在channel中 传递信号。
type void struct{}
type void1 struct {
a void
}
type void2 struct {
a void
b int
}
func main() {
a0 := void{}
a1 := void1{}
a2 := void2{}
fmt.Println(unsafe.Sizeof(a0))
fmt.Println(unsafe.Sizeof(a1))
fmt.Println(unsafe.Sizeof(a2))
fmt.Printf("void: %p\n", &a0)
fmt.Printf("void1:%p\n", &a1)
fmt.Printf("void2: %p\n", &a2)
}
打印:
0
0
8
void: 0x11804e0 zerobase的地址,不是固定,每次运行都会有偏移量
void1:0x11804e0
void2: 0xc00010c008
能看到当一个空结构体中,包含了其他类型的变量,就不指向 zerobase。
runtime的malloc.go中
// base address for all 0-byte allocations
var zerobase uintptr
接口
go中的接口都是隐式的,增加的封装的灵活性,也为阅读源码增加了一些难度。
正常使用: 情况1
type Person interface {
eat()
}
type Man struct {
name string
}
func (m Man) eat() {
fmt.Println(" man eat")
}
func main() {
var p Person = Man{}
p.eat()
}
情况2:
func main() {
// 变成指针 也是正常的
var p Person = &Man{}
p.eat()
}
情况3:
// 指针实现
func (m *Man) eat() {
fmt.Println(" man eat")
}
func main() {
var p Person = &Man{}
p.eat()
}
也正常
情况4:
// 指针
func (m *Man) eat() {
fmt.Println(" man eat")
}
func main() {
// 未加指针
var p Person = Man{}
p.eat()
}
报错: cannot use Man{} (value of type Man) as Person value in variable declaration: Man does not implement Person (method eat has pointer receiver) (typecheck)
Man结构未实现,person的方法 eat这个。
网上很多人有讲过这个,这里换个角度归纳下:
只有一种情况下是失败的:当实现接口方法时候,采用指针,用的使用 未采用指针。
原理:在使用 func (m Man) eat()
实现接口时候,编译器会自动加上 带指针的实现 func (m *Man) eat()
,反之,不会。所以才会导致情况4失败。
接口的定义
这个数据结构,就是上面例子中变量 p 底层结构。
接口的内部实现:
type iface struct {
tab *itab
data unsafe.Pointer // 具体实现接口的对象, 就是例子中的 Man结构体的实例
}
type itab struct {
inter *interfacetype // 接口自身定义的类型信息,用于定位到具体interface类型
_type *_type
hash uint32 // _type.hash的拷贝,用于快速查询和判断目标类型和接口中类型是一致
_ [4]byte
fun [1]uintptr // 实现那些接口方法
}
整理下:接口值的底层表示
接口数据使用 runtime.iface 表示
iface记录了数据的地址
iface 中记录了接口类型信息和实现的方法 , 在接口断言时候,用到这些信息。
空接口
空接口的底层实现:
type eface struct {
_type *_type // 只记录数据类型,因为没有方法,所以不用像iface一样,记录接口方法信息
data unsafe.Pointer // 指向数据本身
}
空接口常用来作为 任意类型的形参 使用。
例如:
type any = interface{}
func Println(a ...any) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
为什么 空接口可以作为任意类型使用?
基于它的底层实现定义:任意的类型,都可以表示为 数据类型 和 数据本身,例如
int 5
,类型int
,数据5
例如在我们使用 fmt.Println(5)
时候,会先将 5 进行组装:
伪代码:
a := eface{type : int ,data : 5}
fmt.Println(a)
整理:空接口的用途
空接口的最大用途是作为任意类型的函数入参
函数调用时,会新生成一个空接口,再传参
nil
定义:
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
也就是说nil只能表示 指针、channel、func、interface、map 、slice 这六种类型的空值。
注意这里没有 struct
空结构体是zerobase的空值,不是nil
var a *int
var b map[string]string
var c struct{}
fmt.Println(a == nil) // true
fmt.Println(b == nil) // true
fmt.Println(c == nil) // mismatched types struct{} and untyped nil
fmt.Println(a == b) // mismatched types *int and map[string]string
都是nil,a和b的值也不同。
有了上面空接口的基础就好理解了, 一个数据,包含了数据类型和数据本身的值。这里a和b都是nil,只是值为nil,但是它们的类型并不一样,所以不等
小结:
nil 是空,并不一定是“空指针”
nil是6种类型的 “零值〞
每种类型的nil是不同的,无法比较
再一个例子:
var a *int
var b interface{}
fmt.Println(a == nil) // true
fmt.Println(b == nil) // true
b = a
fmt.Println(b == nil) // false
回忆下上面 nil的定义,可以表示 interface
的空值,但是,通过上面的了解,interface
底层实际上是一个结构体eface
。
nil能作为eface的值,有严格的要求,要求type 和 data 都为空
当把 b = a
时候,这时候 type 已经有值,data还为空,但是这个时候 eface
已经是一个结构体了。nil 不能表示 结构体的值,而且这个结构体中成员还不为空。
总结:
- nil是多个类型的零值,或者空值
- 空结构体的指针和值都不是nil。 指针是zerobase
3.空接口零值是nil,-旦有了类型信息就不是nil
热门相关:性感女郎5 O型西瓜胸部的家政妇 仙碎虚空 百炼成仙 甜妻动人,霸道总裁好情深