记录一些 Golang 特殊的知识点和容易误解踩坑的地方
字符串长度判断
判断字符串长度用len([]rune(str))
,len(str)
是字节数
str := "嘎嘎嘎"
// 2
println(len([]rune(str)))
// 6
println(len(str))
A Comprehensive Guide of Arrays and Slices in Golang (and their differences)
对于 slice,必须掌握其底层数据结构,尤其是长度
和容量
两个概念,要知道拷贝、扩容实际是什么机制
遍历取value指针
for-range 中的 v
是值的副本,每次遍历仅进行值拷贝,它指向的地址不变
arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
res = append(res, &v)
}
// output: 2 2
fmt.Println(*res[0],*res[1])
通过索引获取&arr[i]
是一种可读性更好的方式
遍历数组拷贝
arr := [3]int{1, 2, 3}
for _, v := range arr {
fmt.Println(v)
}
数组遍历前对arr进行了拷贝,如果数组很长会浪费内存,可以优化成取地址遍历
arr := [3]int{1, 2, 3}
for _, v := range &arr {
fmt.Println(v)
}
遍历slice则不会有上述问题,因为没有发生底层数据的拷贝
- 做切片data指针还是指向原数据的地址
- append如果发生扩容,data指针指向的已经不是同一个数组地址
a1 := []int{1, 2, 3, 4}
s1 := a1[0:2]
s1[0] = 22
// 22
fmt.Println(a1[0])
s1 = append(s1, 5, 6, 7)
s1[0] = 33
// 22
fmt.Println(a1[0])
- map的遍历是无序的
- 并发读写会导致panic,可以加读写锁或者使用sync.Map
Go语言中的map底层就是哈希表,数据结构在runtime/map.go中:
// Go map 数据结构.
type hmap struct {
// map大小,len(map)的返回值
count int
flags uint8
// buckets数组长度的对数 (最多可以容纳 loadFactor * 2^B 个元素),
// LoadFactor(负载因子)= hash表中已存储的键值对的总数量/hash桶的个数(即hmap结构中buckets数组的个数)
B uint8
// 溢出桶的大概数量
noverflow uint16
// 哈希种子
hash0 uint32 // hash seed
// 指向buckets数组,大小为2^B,count==0时为nil
buckets unsafe.Pointer
// 在扩容时用于保存旧buckets数组,大小为buckets的一半,仅在扩容时非nil
oldbuckets unsafe.Pointer
// 指示扩容进度,小于此地址的 buckets 都已迁移完成
nevacuate uintptr
// 附加字段
extra *mapextra
}
// mapextra holds fields that are not present on all maps.
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}
// Go map bucket 的数据结构.
type bmap struct {
// tophash存储了键的哈希的高 8 位,通过比较不同键的哈希的高8位可以减少访问键值对次数以提高性能
tophash [bucketCnt]uint8
}
// 在运行时bmap的结构,每个哈希桶最多存放 8 个键值对,
// 当经由哈希函数映射到该地址的元素数超过 8 个时, 会将新的元素放到溢出桶中, 并使用 over flow 指针链向这个溢出桶
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
map整体数据结构如下:
type A struct {
Name string
}
方法一: A{} == a
A 中的字段都是可比较的才行
a := A{}
if (A{}) == a {
fmt.Println("(A{}) == a: empty struct")
}
如果是
type B struct {
Name string
AS []string
}
b := B{}
if (B{}) == b {
fmt.Println("(B{}) == b: empty struct")
}
则会报错 invalid operation: B literal == b (struct containing []string cannot be compared)
方法二: 反射
a := A{}
if reflect.DeepEqual(a, A{}) {
fmt.Println("reflect.DeepEqual(a, A{}): empty struct")
}
使用反射会有一定的性能牺牲,对性能要求高得场景不适宜大量使用
方法三: 字段标记
给结构体加个字段专门用于标记是否被初始化过
每次初始化时记得给该字段赋值
Interface是Go中非常重要的一个概念,利用interface的特性我们可以实现多态,将我们的业务实现进行抽象。但是,过度使用interface也是一种灾难。在实战中,滥用interface往往会增加项目代码的复杂度,我们在熟悉业务代码或者debug的时候,如果到处都是interface,那简直是一种灾难。适度抽象可以隐藏具体实现,利于简化阅读和后续迭代复用,在我们做逻辑分层、封装第三方库、编写公共组件等这些都是适合抽象的场景。
- 在需要的时候再创建interface,而不是预先创建很多interface,再去实现。
- 为了防止在灵活性方面受到限制,在大多数情况下,函数不应该返回interface,而是返回具体的实现。相反,函数应尽可能接受interface。
- new 只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值
- make 只能用来分配及初始化类型为 slice、map、chan 的数据,返回的是引用类型