go数据类型-空结构体、空接口、nil

发布时间 2023-11-29 21:41:48作者: 杨阳的技术博客

空结构体

  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 不能表示 结构体的值,而且这个结构体中成员还不为空。

总结:

  1. nil是多个类型的零值,或者空值
  1. 空结构体的指针和值都不是nil。 指针是zerobase

3.空接口零值是nil,-旦有了类型信息就不是nil