1、什么变量大小是 0 字节
如何看变量在内存占了多少个字节,unsafe 包下的 Sizeof 可以查看。
基本类型的字节数:
func main() { fmt.Println(unsafe.Sizeof(int(0))) // 8 跟着系统的,如果32位,则占4个字节 fmt.Println(unsafe.Sizeof(int32(0))) // 4 如果int32,则永远占4个字节 fmt.Println(unsafe.Sizeof(int64(0))) // 4 如果int64,则永远占8个字节 i := int(0) p := &i fmt.Println(unsafe.Sizeof(p)) // 结果:8 也是跟随系统字长的
}
int 大小跟随系统字长,指针大小也是一样
空结构体
空结构体的地址均相同,空结构体占 0 个字节。
type K struct {} func main() { fmt.Println(unsafe.Sizeof(K{})) // 结果:0
}
空结构体可以节约内存,使用场景:chan 发信号通知、 map 只要 key 不要 value
如使用 Map 的时候,只要 key 不要 value ,那么 空 struct{} 可以作为 value ,可以节约内存。
func main() { // 场景1:只要 key ,不要 value m := map[string]struct{}{} m["Alin"] = struct{}{} // 场景2:chan 在创建 chan 时候,后续需要跟上负载,如果只想发个信号,int 占8个字节,bool 占 1个字节 ch := make(chan struct{}) fmt.Println(ch)
}
2、数组、字符串、切片底层是一样的吗?
String 底层
字符串本质是个结构体,是个变长编码
如果对字符串直接使用下标访问,得到的是字节,所以需要 range 遍历时,被解码成 rune 类型的字符。
func main() { str := "哈喽hello" fmt.Println(unsafe.Sizeof(str)) fmt.Println(len(str)) // 11 字节for _, char := range str { fmt.Println(char) // 得到字节,转string
}
}// string 底层是个 struct 结构体
type stringStruct struct { str unsafe.Pointer // 指针指向底层 Byte 数组len int // 表示 Byte 数组的长度(字节数)
}
切片(Slice)
切片底层源码:
// runtime/slice.go
type slice struct { array unsafe.Pointer // 切片其实就是对数组的引用len int // 这个长度,是引用的部分的长度cap int
}
切片的创建:
// 1. 根据数组创建
arr[0:3] or slice[0:3]// 2. 字面量:编译时插入创建数组的代码
// 字面量方式,底层也是先创建一个数组 go build -o -gcflags -S main.goslice := []int{1,2,3}
// 3. make:运行时创建数组
// 底层调用makeslice函数
slice := make([]int,3)
切片追加(append)
添加如果不扩容,只调整 len 长度即可。
若需要扩容,会去调用 runtime.growslice()
进行扩容,当切片长度小于 256 将会翻倍扩容,大于 256 每次增加 25%,切片扩容时,并发不安全,需要并发加锁。
// go1.19
func growslice(et *_type, old slice, cap int) slice { newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { // Transition from growing 2x for small slices // to growing 1.25x for large slices. This formula // gives a smooth-ish transition between the two. newcap += (newcap + 3*threshold) / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } ......return slice{p, old.len, newcap}
}
slice 是深拷贝还是浅拷贝?
(1)浅拷贝:
如何直接赋值的话,是浅拷贝的,修改新的接收这个变量是会影响原来的数组
func main() { slice := []int{1, 2} newSlice := slice newSlice[0] = 3 fmt.Println("slice:", slice) fmt.Println("newSlice:", newSlice) fmt.Printf("%p, %p\n", slice, newSlice)
}结果:
slice: [3 2]
newSlice: [3 2]
0xc0000200b0, 0xc0000200b0
(2)深拷贝:
func main() { slice := []int{1, 2} fmt.Println("slice: ", slice) fmt.Printf("%p\n", slice) slice = append(slice, 8, 9) slice[0] = 100 fmt.Println("slice: ", slice) fmt.Printf("%p\n", slice)
}
在 Go 中,切片、 map 、chan 都是是引用传递
当你将一个切片作为参数传递给一个函数时,函数中的任何更改都会反映在原始切片中。这是因为切片本身只是一个指向底层数组的指针、长度和容量的结构体,而不是实际的数据。因此,当你传递一个切片时,函数接收到的是指向相同底层数组的指针,因此它可以修改原始切片中的数据
3、Map
3.1、哈希冲突解决方案
开放寻址法、拉链法
底层数据结构:
// A header for a Go map.
type hmap struct { count int // 统计k,v数量flags uint8 B uint8 // 桶数量的对数 log_2noverflow uint16 hash0 uint32 // 哈希种子buckets unsafe.Pointer //桶说明拉链法,把相同hash值k,v放一个桶,避免碰撞问题oldbuckets unsafe.Pointer nevacuate uintptrextra *mapextra
}type mapextra struct { overflow *[]*bmap oldoverflow *[]*bmap nextOverflow *bmap
}// A bucket for a Go map.
type bmap struct { tophash [bucketCnt]uint8
}
4、接口
5、nil、空接口、空结构体有什么区别?
// Type 可能是pointer, channel, func, interface, map, or slice type
var nil Type
nil
nil 是空,并不一定是 ”空指针“,nil 是 6 种类型的零值。每种类型的 nil 是不同的,无法比较。
空结构体
空结构体的值不是 nil ,空结构体的指针也不是 nil,但是都相同(zerobase)
空接口
空接口零值是 nil,一旦有类型信息就不是 nil。
var a interface{}
fmt.Println(a == nil) // true var b *int
a = b
fmt.Println(a == nil) // false
6、内存对齐
S1 和 S2 在内存占用多大内存
type S1 struct { num1 int32 num2 int32
} type S2 struct { num1 int16 num2 int32
} func main() { fmt.Println(unsafe.Sizeof(S1{})) // 8 fmt.Println(unsafe.Sizeof(S2{})) // 8
}
非内存对齐:内存的原子性与效率受到影响