Skip to content

Latest commit

 

History

History
224 lines (165 loc) · 5.86 KB

14.md

File metadata and controls

224 lines (165 loc) · 5.86 KB

Go Careful

记录一些 Golang 特殊的知识点和容易误解踩坑的地方

String

字符串长度判断

判断字符串长度用len([]rune(str)),len(str)是字节数

str := "嘎嘎嘎"
// 2
println(len([]rune(str)))
// 6
println(len(str))

Array & Slice

A Comprehensive Guide of Arrays and Slices in Golang (and their differences)

对于 slice,必须掌握其底层数据结构,尤其是长度容量两个概念,要知道拷贝、扩容实际是什么机制

for-range

遍历取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则不会有上述问题,因为没有发生底层数据的拷贝

slice append

  1. 做切片data指针还是指向原数据的地址
  2. 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

  1. map的遍历是无序的
  2. 并发读写会导致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整体数据结构如下:

map

参考文章

Struct

判断空结构体

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

Interface是Go中非常重要的一个概念,利用interface的特性我们可以实现多态,将我们的业务实现进行抽象。但是,过度使用interface也是一种灾难。在实战中,滥用interface往往会增加项目代码的复杂度,我们在熟悉业务代码或者debug的时候,如果到处都是interface,那简直是一种灾难。适度抽象可以隐藏具体实现,利于简化阅读和后续迭代复用,在我们做逻辑分层、封装第三方库、编写公共组件等这些都是适合抽象的场景。

  • 在需要的时候再创建interface,而不是预先创建很多interface,再去实现。
  • 为了防止在灵活性方面受到限制,在大多数情况下,函数不应该返回interface,而是返回具体的实现。相反,函数应尽可能接受interface。

make & new

  • new 只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值
  • make 只能用来分配及初始化类型为 slice、map、chan 的数据,返回的是引用类型