赞
踩
=
和:=
的区别是什么?:=
是简短变量声明语句,用于在函数内部快速声明一个局部变量,无法用在函数外部,作用在编译阶段。
=
是赋值语句,用于给已经声明的变量赋值,作用在运行时。
相同点:
make
和new
都是golang
的内建函数,可以直接在代码中使用,无需导入其他包;make
和new
都用于分配内存,但分配的方式和用途有所不同。
不同点:
使用类型:new
类型用于为 值类型(如基本数据结构,结构体等) 分配内存并返回其指针,而make
用于为引用类型(如切片、map、通道等) 分配内存并初始化(new只分配内存,不初始化)。
参数不同: new
函数只接受一个参数,即一个类型,返回一个指向该类型零值的指针;而make函数可以接受多个参数,具体取决于所创建的引用类型。
返回值不同: new函数返回一个指向新分配的零值的指针,而make函数返回一个初始化后的引用类型(如切片、映射、通道),而不是指针。
mpPtr := new([]int)
slcPtr := new(map[string]int)
fmt.Println((*mpPtr) == nil) // true
fmt.Println((*slcPtr) == nil) // true
在for a,b := range c
遍历中, a 和 b 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程。解决办法:在每次循环时,创建一个临时变量。
同理,for i := 0; i < len(dirs); i++
遍历中的i
变量也会有相同的问题。
slice
的底层由数组实现。一个slice
由三个部分构成:指针、长度和容量。底层数据结构如下:
type slice struct {
array unsafe.Pointer // 指向底层数组的指针
len int // 切片当前的长度
cap int // 切片的容量
}
指针指向slice
的起始元素地址(这里需要注意的是起始元素地址并不一定是底层数组的起始元素);
长度对应的是slice
中元素的数目,容量一般是指从slice
开始位置到底层数组的结尾位置的大小。
长度不能超过容量。
内置的len
函数可以用来返回slice
的长度,cap
函数可以用来返回slice
的容量。
s := []int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(len(s), cap(s)) // 8 8
在go 1.18之后,以256为临界点。
基于一个base slice
切片创建一个新的slice
时,两个slice
共享同一个底层数组,对于其中一个的元素做修改会影响到另一个。
s := []int{0, 1, 2, 3, 4, 5, 6, 7}
s2_5 := s[2:5]
reverse(s2_5) // 对切片中的元素做反转
fmt.Println(s) // [0 1 4 3 2 5 6 7]
fmt.Println(s2_5) // [4 3 2]
如何避免:
copy
函数深拷贝slice
。s := []int{0, 1, 2, 3, 4, 5, 6, 7}
s2_5 := make([]int, 3)
copy(s2_5, s[2:5])
reverse(s2_5) // 对切片中的元素做反转
fmt.Println(s) // [0 1 2 3 4 5 6 7] 原切片没变
fmt.Println(s2_5) // [4 3 2]
s := []int{0, 1, 2, 3, 4, 5, 6, 7}
s2_5 := s[2:5]
s = append(s, 8) // 切片s扩容,指向新的数组
// 对切片中的元素做反转
fmt.Println(s) // [0 1 2 3 4 5 6 7] 原切片没变
fmt.Println(s2_5) // [4 3 2]
nil切片:声明为切片,但是没有分配内存,切片的指针是nil
var s []int
fmt.Println(s == nil) // true
空切片:切片指针指向了一个数组内存地址,但是数组是空的
s1 := []int{} //1.空切片,没有任何元素
s2 := make([]int, 0) //2.make 切片,没有任何元素
nil切片和空切片的本质区别就是: nil切片没有分配内存,空切片是有分配内存但底层指向的是一个空数组
Map
是用于存储键值对的集合,底层通过哈希表实现。哈希表是一种使用哈希函数将键映射到存储位置的数据结构,他使用一个数组(bucket或桶)来存储键值对,当我们插入一个键值对时,会使用哈希函数计算出键的哈希值,并根据哈希值来选择数组中的一个位置来存储值。
Go语言的map底层结构如下:
type hmap struct {
count int // 当前map中存储的键值对数量
flags uint8 // 保存一些标志位,如迭代器的状态等
B uint8 // bucket的位数,表示底层数组的长度为2^B
noverflow uint16 // 溢出桶的数量
hash0 uint32 // 哈希种子,用于增加哈希的随机性,防止哈希碰撞攻击
buckets unsafe.Pointer // 指向bucket数组的指针
oldbuckets unsafe.Pointer // 指向旧bucket数组的指针,用于map扩容时的迁移
nevacuate uintptr // 用于map扩容时迁移的标志位
extra *mapextra // 一些额外的字段,如迭代器指针等
}
其中,buckets指向一个bucket数组的指针,bucket是一个存储键值对的容器,每个bucket里面有一个或多个键值对。当插入新的键值对时,根据哈希值计算索引,找到对应的bucket,然后将键值对插入到bucket中。如果哈希值冲突,即多个键映射到同一个索引,这些键值对会按照链表形式存储在同一个bucket中。
当map进行扩容时,会创建一个新的bucket数组(通常是原数组大小的两倍),然后将所有键值对重新哈希并放入新的数组中,这个过程是比较耗时的。为了减少迁移带来的性能损耗,Go语言采用增量式迁移策略,即在多次操作中逐步完成迁移。
总结:
Go语言的map底层是通过哈希表实现的,使用了数组和链表结构来存储键值对。
当插入或查找键值对时,使用哈希函数计算键的哈希值,根据哈希值找到对应的位置。
扩容时,会创建新的数组,将键值对从旧数组迁移到新数组,以减少扩容带来的性能损耗。
nil:Map的零值,没有引用任何哈希表,map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常。
var mp map[string]int
fmt.Println(mp == nil) // true
空map:空的map,指向一个大小为0哈希表
mp1 := make(map[string]int)
mp2 := map[string]int{}
fmt.Println(mp1 == nil) // false
fmt.Println(mp2 == nil) // false
在向map存值时必须创建map。
空结构体: 结构体没有任何成员的话就叫空结构体,写作struct{}
,它的大小是0,也不包含任何信息。作用如下:
set
集合:在某些场景下,可以使用map[KeyType]struct{}
的形式实现一个set
集合,因为其大小为0,可以避免不必要的内存损耗。goroutine
之间进行状态传递场景下,可以使用struct{}
作为通道元素类型,用作通道信号。type
可以实现只有方法的结构体。defer
语句的执行顺序与声明顺序相反,类似与栈LIFO
(后进先出)。
这个需要区分情况:
函数无名
时,也就是返回值只有返回数据类型,没有指定返回值名称,函数签名如func test() int
,这种情况下,不会更改返回值,原因是:在执行return语句后,Go会创建一个临时变量保存返回值。 下面举个例子:
func test() int { i := 0 defer func() { fmt.Println("defer1") }() defer func() { i += 1 fmt.Println("defer2") }() return i } func main() { fmt.Println("return", test()) } // defer2 // defer1 // return 0
函数有名时,也就是返回值指定了返回数据类型和返回值名称,函数签名如func test() (i int)
,这种情况下,返回值会被更改,原因是:在执行return语句后,Go并不会再创建临时变量,而是继续使用当前的变量。 下面举个例子:
func test() (i int) {
i = 0
defer func() {
i += 1
fmt.Println("defer2")
}()
return i
}
func main() {
fmt.Println("return", test())
}
// defer2
// return 1
init()
函数是golang
初始化的一部分,由runtime初始化每个导入的包,初始化是按照包之间依赖关系,最先初始化没有依赖的包。
每个包首先初始化包作用域内的常量和变量(常量优先于变量),然后执行init()
函数。
执行顺序:import
–> const
–> var
–>init()
–>main()
Go实现面向对象的两个关键是struct
和interface
。
struct
中内嵌需要继承的类即可。interface
实现,类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。Go支持多重继承,可以在类型中嵌入所有必要的父类型。
go语言圣经(中文版)
Go 语言设计与实现
Go 1.18 全新的切片扩容机制
Go 语言数组和切片的区别
Go常见面试题【由浅入深】2022版
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。