Golang 空接口 空结构体

空接口

空接口是接口类型的特殊形式。空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度来看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。

  • 空接口类型类似于 C# 或 Java 语言中的 Object、C语言中的 void*、C++ 中的 std::any。在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法
  • 空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。

空接口类型的变量可以存储任意类型的变量。即使是接收指针类型也用 interface{},而不是使用 *interface{}。

永远不要使用一个指针指向一个接口类型,因为它已经是一个指针。
package main

import "fmt"

func main() {
	// 定义一个空接口x
	var x interface{}
	s := "pprof.cn"
	x = s
	fmt.Printf("type:%T value:%v\n", x, x)
	i := 100
	x = i
	fmt.Printf("type:%T value:%v\n", x, x)
	b := true
	x = b
	fmt.Printf("type:%T value:%v\n", x, x)
}

空接口内存分配 

​Go 1.15 中 var i interface{} = a 会有额外堆内存分配吗?

var a  int = 3
// 以下有额外内存分配吗?
var i interface{} = a

​Go 1.15在 runtime 部分中提到了一个有趣的改进:将小整数转换为接口值不再需要进行内存分配。小整数是指 0 到 255 之间的数。

空接口的应用

1.空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

//函数的参数个数和每个参数的类型都不是固定的
func myfunc(args ...interface{}) {
}

2.空接口作为map,数组,切片的各种类型的值

func main() {
	// 空接口作为map值
	var studentInfo = make(map[string]interface{})
	studentInfo["name"] = "李白"     //string
	studentInfo["age"] = 18        //int
	studentInfo["height"] = 1.82   //float
	studentInfo["married"] = false //bool
	fmt.Println(studentInfo)

	var a = new([3]interface{})
	a[0] = "Hello,World"
	a[1] = 32
	for _, b := range a {
		fmt.Printf("%v\t%[1]T\n", b)
	}
}

3.类型断言

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

func main() {
	var x interface{}
	x = "pprof.cn"
	v, ok := x.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("类型断言失败")
	}
}

4.类型判断

func justifyType(any interface{}) {
	switch v := any.(type) {
	case string:
		fmt.Printf("any is a string,value is: %v\n", v)
	case int:
		fmt.Printf("any is a int is: %v\n", v)
	case bool:
		fmt.Printf("any is a bool is: %v\n", v)
	case float32, float64:
		fmt.Printf("any is a float is: %v\n", v)
	default:
		fmt.Printf("unsupport type:%T is: %v\n", v, v)
	}
}

func main() {
	var x interface{}
	x = "pprof.cn"
	justifyType(x)
	x = 0.1
	justifyType(x)
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

空结构体

我们说不包含任何字段的结构体叫做空结构体,可以通过如下的方式定义空结构体:

type empty struct{}

特点

  • 地址相同

我们分别定义两个非空结构体和空结构体变量,然后取地址打印,发现空结构体变量的地址是相同的:

// 定义一个非空结构体
type User struct {
	name string
}

func main() {
	// 两个非空结构体的变量地址不同
	var user1 User
	var user2 User
	fmt.Printf("%p \n", &user1) // 0xc000318670
	fmt.Printf("%p \n", &user2) // 0xc000318680

	// 定义两个空结构体,地址相同
	var first struct{}
	var second struct{}
	fmt.Printf("%p \n", &first)  // 0x1ca15f0
	fmt.Printf("%p \n", &second) // 0x1ca15f0
}

我们知道 Go 语言中的变量传递都是值传递,对于传参前后的变量地址应该不同,我们通过传参的方式再来试一下:

// 非空结构体
type NonEmptyUser struct {
	name string
}

// 空结构体
type EmptyUser struct{}

// 打印非空结构体参数地址
func testNonEmptyUser(user NonEmptyUser) {
	fmt.Printf("%p \n", &user)
}

// 打印空结构体参数地址
func testEmptyUser(user EmptyUser) {
	fmt.Printf("%p \n", &user)
}

func main() {
	// 两个非空结构体的变量地址不同
	var user1 NonEmptyUser
	fmt.Printf("%p \n", &user1) // 0xc0001986c0
	testNonEmptyUser(user1)     // 0xc0001986d0

	// 两个空结构体变量的地址相同
	var user2 EmptyUser
	fmt.Printf("%p \n", &user2) // 0x1ca25f0
	testEmptyUser(user2)        // 0x1ca25f0
}

发现对于非空结构体,传参前后的地址是不同的,但是对于空结构体变量,前后地址是一致的

  • 内存占用大小为0

在Go中,我们可以使用 unsafe.Sizeof 来计算一个变量占用的字节数,那么就举几个例子来看下:

type EmptyUser struct{}

func main() {
	var i int
	var s string
	var m []string
	var u EmptyUser
  
	fmt.Println(unsafe.Sizeof(i)) // 8
	fmt.Println(unsafe.Sizeof(s)) // 16
	fmt.Println(unsafe.Sizeof(m)) // 24
	fmt.Println(unsafe.Sizeof(u)) // 0
}

可以看到空结构体占用的内存空间大小为0,同时对于空结构体的组合,占用空间大小也为0:

// 空结构体的组合
type EmptyUser struct {
	name struct{}
	age  struct{}
}

func main() {
	var u EmptyUser
	fmt.Println(unsafe.Sizeof(u)) // 0
}

原理探究

为什么空结构体的地址都相同,而且大小都为0呢,我们一起来看下源码(go/src/runtime/malloc.go):

// base address for all 0-byte allocations
var zerobase uintptr

// 创建新的对象时,调用 mallocgc 分配内存
func newobject(typ *_type) unsafe.Pointer {
	return mallocgc(typ.size, typ, true)
}

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	if gcphase == _GCmarktermination {
		throw("mallocgc called with gcphase == _GCmarktermination")
	}

	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
	......
}

通过源码可以看出,创建新的对象时,需要调用 malloc.newobject() 进行内存分配,进一步调用 mallocgc 方法,在该方法中,如果判断类型的size==0 ,固定返回zerobase的地址。zerobase是一个uintptr 全局变量,占用 8 个字节。因此我们可以确定的是,在Go语言中,所有针对 size==0 的内存分配,用的都是同一个地址 &zerobase,所以我们在一开始看到的所有空结构体地址都相同。

使用场景

空结构体不包含任何数据,那么其应用场景也应该不在乎值内容,只当做一个占位符。在这种场景下,由于其不占用内存空间,使用空结构体既可以做到节省空间,又可以提供语义支持。

  • 集合(Set)

使用过 Java 的同学应该都用过 Set 类型,Set 是保存不重复元素的集合,但是 Go 语言没有提供原生的 Set 类型。但是我们知道 Map 结构存储的是 key-value 类型,key 不允许重复,因此可以利用 Map 来实现 Set,key存储需要的数据,value 给个固定值就可以了。那么 value 给什么值好呢?这时候我们的 空结构体 就可以出场了,不占用空间,还可以完成占位操作,堪称完美,下面我们看怎么实现吧。比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。

// 定义了一个保存 string 类型的 Set集合
type Set map[string]struct{}

// 添加一个元素
func (s Set) Add(key string) {
	s[key] = struct{}{} //第2个{}表示赋值
}

// 移除一个元素
func (s Set) Remove(key string) {
	delete(s, key)
}

// 是否包含一个元素
func (s Set) Contains(key string) bool {
	_, ok := s[key]
	return ok
}

// 初始化
func NewSet() Set {
	s := make(Set)
	return s
}

// 测试使用
func main() {
	set := NewSet()
	set.Add("hello")
	set.Add("world")
	fmt.Println(set.Contains("hello"))

	set.Remove("hello")
	fmt.Println(set.Contains("hello"))
}

func min(a int, b uint) {
	var min = 0
	if a < 0 {
		min = a
	} else {
		min = copy(make([]struct{}, a), make([]struct{}, b))
	}
	fmt.Printf("The min of %d and %d is %d\n", a, b, min)
}
  • channel中信号传输

空结构体 与 channel 可谓是一个经典组合,有时候我们只是需要一个信号来控制程序的运行逻辑,并不在意其内容如何。
在下面的例子中,我们定义了两个 channel 用于接收两个任务完成的信号,当接收到任务完成的信号时,就会触发相应的动作。

func doTask1(ch chan struct{}) {
	time.Sleep(time.Second)
	fmt.Println("do task1")
	ch <- struct{}{}
}

func doTask2(ch chan struct{}) {
	time.Sleep(time.Second * 2)
	fmt.Println("do task2")
	ch <- struct{}{}
}

func main() {
	ch1 := make(chan struct{})
	ch2 := make(chan struct{})
	go doTask1(ch1)
	go doTask2(ch2)

	for {
		select {
		case <-ch1:
			fmt.Println("task1 done")
		case <-ch2:
			fmt.Println("task2 done")
		case <-time.After(time.Second * 5):
			fmt.Println("after 5 seconds")
			return
		}
	}
}

总结

本篇文章,我们学习了如下内容:

  1. 空结构体是一种特殊的结构体,不包含任何元素
  2. 空结构体的大小都为0
  3. 空结构体的地址都相同
  4. 由于空结构体不占用空间,从节省内存的角度出发,适用于实现Set结构、在 channel 中传输信号等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值