大纲
-
背景
-
原理解密
-
-
定义的各种姿势
-
`struct {}` 作为 receiver
-
-
配合使用姿势
-
-
`map` & `struct{}`
-
`chan` & `struct{}`
-
`slice` & `struct{}`
-
-
总结
背景
struct
提示:以下都是基于 go1.13.3 linux/amd64 分析。
普通的结构体定义如下:
// 类型变量对齐到 8 字节;
type Tp struct {
a uint16
b uint32
}
按照内存对齐规则,这个结构体占用 8 个字节的内存。关于内存分配的基础知识可以翻看:Golang 数据结构到底是怎么回事?gdb调一调?,golang 内存管理分析。
空结构体:
var s struct{}
// 变量 size 是 0 ;
fmt.Println(unsafe.Sizeof(s))
该空结构体的变量占用内存 0 字节。
本质上来讲,使用空结构体的初衷只有一个:节省内存,但是更多的情况,节省的内存其实很有限,这种情况使用空结构体的考量其实是:根本不关心结构体变量的值。
原理解密
特殊变量:zerobase
zerobaseuintptrstruct {}zerobase&zerobase
举个例子:
package main
import "fmt"
type emptyStruct struct {}
func main() {
a := struct{}{}
b := struct{}{}
c := emptyStruct{}
fmt.Printf("%p\n", &a)
fmt.Printf("%p\n", &b)
fmt.Printf("%p\n", &c)
}
dlv 调试分析一下:
(dlv) p &a
(*struct {})(0x57bb60)
(dlv) p &b
(*struct {})(0x57bb60)
(dlv) p &c
(*main.emptyStruct)(0x57bb60)
(dlv) p &runtime.zerobase
(*uintptr)(0x57bb60)
小结:空结构体的变量的内存地址都是一样的。
内存管理特殊处理
mallocgc
struct {}runtime.zerobasemallocgc
代码如下:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 分配 size 为 0 的结构体,把全局变量 zerobase 的地址给出去即可;
if size == 0 {
return unsafe.Pointer(&zerobase)
}
// ...
mallocgczerobase
有这种全局唯一的特殊的地址也方便后面一些逻辑的特殊处理。
定义的各种姿势
原生定义
a := struct{}{}
struct{}struct {}runtime.zerobase
重定义类型
type
type emptyStruct struct{}
emptyStructtypestruct{}emptryStructzerobase
匿名嵌套类型
struct{}
匿名嵌套方式一
type emptyStruct struct{}
type Object struct {
emptyStruct
}
匿名嵌套方式二
type Object1 struct {
_ struct {}
}
ObjectObject1runtime.zerobase
内置字段
内置字段的场景没有什么特殊的,主要是地址和长度的对齐要考虑。还是只需要注意 3 个要点:
-
空结构体的类型不占内存大小;
-
地址偏移要和自身类型对齐;
-
整体类型长度要和最长的字段类型长度对齐;
我们分 3 种场景讨论这个问题:
struct {}
struct {}
// Object1 类型变量占用 1 个字节
type Object1 struct {
s struct {}
b byte
}
// Object2 类型变量占用 8 个字节
type Object2 struct {
s struct {}
n int64
}
o1 := Object1{ }
o2 := Object2{ }
内存怎么分配?
&o1&o1.so1&o2&o2.so2
struct {}
struct {}
// Object1 类型变量占用 16 个字节
type Object1 struct {
b byte
s struct{}
b1 int64
}
o1 := Object1{ }
o1&o1.s&o1.b1
struct { }
struct {}
这个场景稍微注意下,因为编译器遇到之后会做特殊的字节填充补齐,如下;
type Object1 struct {
b byte
s struct{}
}
type Object2 struct {
n int64
s struct{}
}
type Object3 struct {
n int16
m int16
s struct{}
}
type Object4 struct {
n int16
m int64
s struct{}
}
o1 := Object1 { }
o2 := Object2 { }
o3 := Object3 { }
o4 := Object4 { }
struct {}struct { }
o1o2o3o4
o1o2o3o4
struct {}
struct {}
receiver 这个是 golang 里 struct 具有的基础特点。空结构体本质上作为结构体也是一样的,可以作为 receiver 来定义方法。
type emptyStruct struct{}
func (e *emptyStruct) FuncB(n, m int) {
}
func (e emptyStruct) FuncA(n, m int) {
}
func main() {
a := emptyStruct{}
n := 1
m := 2
a.FuncA(n, m)
a.FuncB(n, m)
}
receiver 这种写法是 golang 支撑面向对象的基础,本质上的实现也是非常简单,常规情况(普通的结构体)可以翻译成:
func FuncA (e *emptyStruct, n, m int) {
}
func FuncB (e emptyStruct, n, m int) {
}
编译器只是把对象的值或地址作为第一个参数传给这个参数而已,就这么简单。 但是在这里要提一点,空结构体稍微有一点点不一样,空结构体应该翻译成:
func FuncA (e *emptyStruct, n, m int) {
}
func FuncB (n, m int) {
}
极其简单的代码,对应的汇编实际代码如下:
FuncA,FuncB 就这么简单,如下:
00000000004525b0 <main.(*emptyStruct).FuncB>:
4525b0: c3 retq
00000000004525c0 <main.emptyStruct.FuncA>:
4525c0: c3 retq
main 函数
00000000004525d0 <main.main>:
4525d0: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
4525d9: 48 3b 61 10 cmp 0x10(%rcx),%rsp
4525dd: 76 63 jbe 452642 <main.main+0x72>
4525df: 48 83 ec 30 sub $0x30,%rsp
4525e3: 48 89 6c 24 28 mov %rbp,0x28(%rsp)
4525e8: 48 8d 6c 24 28 lea 0x28(%rsp),%rbp
4525ed: 48 c7 44 24 18 01 00 movq $0x1,0x18(%rsp)
4525f6: 48 c7 44 24 20 02 00 movq $0x2,0x20(%rsp)
4525ff: 48 8b 44 24 18 mov 0x18(%rsp),%rax
452604: 48 89 04 24 mov %rax,(%rsp) // n 变量值压栈(第一个参数)
452608: 48 c7 44 24 08 02 00 movq $0x2,0x8(%rsp) // m 变量值压栈(第二个参数)
452611: e8 aa ff ff ff callq 4525c0 <main.emptyStruct.FuncA>
452616: 48 8d 44 24 18 lea 0x18(%rsp),%rax
45261b: 48 89 04 24 mov %rax,(%rsp) // $rax 里面是 zerobase 的值,压栈(第一个参数);
45261f: 48 8b 44 24 18 mov 0x18(%rsp),%rax
452624: 48 89 44 24 08 mov %rax,0x8(%rsp) // n 变量值压栈(第二个参数)
452629: 48 8b 44 24 20 mov 0x20(%rsp),%rax
45262e: 48 89 44 24 10 mov %rax,0x10(%rsp) // m 变量值压栈(第三个参数)
452633: e8 78 ff ff ff callq 4525b0 <main.(*emptyStruct).FuncB>
452638: 48 8b 6c 24 28 mov 0x28(%rsp),%rbp
45263d: 48 83 c4 30 add $0x30,%rsp
452641: c3 retq
452642: e8 b9 7a ff ff callq 44a100 <runtime.morestack_noctxt>
452647: eb 87 jmp 4525d0 <main.main>
通过这段代码证实几个点:
zerobase
e.FuncA&zerobase
总结几个知识点:
interface
可以这么说,编译期间,关于空结构体的参数基本都能确定,那么代码生成的时候,就可以生成对应的静态代码。
程序 debug 技巧和工具介绍可以翻看:golang 调试分析的高阶技巧。
配合使用姿势
struct{ }mapchanslicestruct{}
mapstruct{}
mapstruct {}
// 创建 map
m := make(map[int]struct{})
// 赋值
m[1] = struct{}{}
// 判断 key 键存不存在
_, ok := m[1]
mapstruct {}okmap
map[int]boolmap[int]struct{}
chanstruct{}
channelstruct{}struct{}struct{}
chanstruct{}
// 创建一个信号通道
waitc := make(chan struct{})
// ...
goroutine 1:
// 发送信号: 投递元素
waitc <- struct{}
// 发送信号: 关闭
close(waitc)
goroutine 2:
select {
// 收到信号,做出对应的动作
case <-waitc:
}
struct{}chanstruct{}
slicestruct{}
slicestruct{}
s := make([]struct{}, 100)
我们创建一个数组,无论分配多大,所占内存只有 24 字节(addr, len, cap),但实话说,这种用法没啥实用价值。
makeslicemalllocgcmallocgczerobase
func growslice(et *_type, old slice, cap int) slice {
// 如果元素的 size 为 0,那么还是直接赋值了 zerobase 的地址;
if et.size == 0 {
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
}
总结
zerobasemapstruct{}mapchanstruct{}slicestruct{}
往期推荐
往期推荐
golang 内存管理分析
Golang 数据结构到底是怎么回事?gdb调一调?
Golang 语法到底是怎么回事?gdb调一调?
坚持思考,方向比努力更重要。