2023全新GO工程师面试总攻略,助力快速斩获offer
核心代码,注释必读
// download:
3w ukoou com
数组和切片有什么区别
Go语言中的数组大概相当与C/C++中的数组,固定大小,不能够动态扩展大小,而切片大概相当与C++中的Vector,可以动态扩展大小,当大小超过容量时,重新分配一块内存,然后将数据复制到新的内存区域。下面我们通过几个问题来更好理解golang 的数组和切片,一起来看看吧。
一、数组
Go的切片是在数组之上的抽象数据类型,因此在了解切片之前必须要先理解数组。
1、数组的三种申明方式
- var identifier [len]type
- var identifier = [len]type{value1, value2, … , valueN}
- var identifier = […]type{value1, value2, … , valueN}
相对应的:
- identifier := [len]type{}
- identifier := [len]type{value1, value2, … , valueN}
- identifier := […]type{value1, value2, … , valueN}
例子:
var iarray1 [5]int32
var iarray2 [5]int32 = [5]int32{1, 2, 3, 4, 5}
iarray3 := [5]int32{1, 2, 3, 4, 5}
iarray4 := [5]int32{6, 7, 8, 9, 10}
iarray5 := [...]int32{11, 12, 13, 14, 15}
iarray6 := [4][4]int32{{1}, {1, 2}, {1, 2, 3}}
fmt.Println(iarray1)
fmt.Println(iarray2)
fmt.Println(iarray3)
fmt.Println(iarray4)
fmt.Println(iarray5)
fmt.Println(iarray6)
结果:
[0 0 0 0 0]
[1 2 3 4 5]
[1 2 3 4 5]
[6 7 8 9 10]
[11 12 13 14 15]
[[1 0 0 0] [1 2 0 0] [1 2 3 0] [0 0 0 0]]
我们看数组 iarray1,只声明,并未赋值,Go语言帮我们自动赋值为0。再看 iarray2 和 iarray3 ,我们可以看到,Go语言的声明,可以表明类型,也可以不表明类型,var iarray3 = [5]int32{1, 2, 3, 4, 5} 也是完全没问题的。
2、数组的容量和长度
数组的容量和长度是一样的。cap() 函数和 len() 函数均输出数组的容量(即长度)
3、类型
数组是值类型,将一个数组赋值给另一个数组时,传递的是一份拷贝。而切片是引用类型,切片包装的数组称为该切片的底层数组。看下面的例子:
//a是一个数组,注意数组是一个固定长度的,初始化时候必须要指定长度,不指定长度的话就是切片了
a := [3]int{1, 2, 3}
//b是数组,是a的一份拷贝
b := a
//c是切片,是引用类型,底层数组是a
c := a[:]
for i := 0; i < len(a); i++ {
a[i] = a[i] + 1
}
//改变a的值后,b是a的拷贝,b不变,c是引用,c的值改变
fmt.Println(a) //[2,3,4]
fmt.Println(b) //[1 2 3]
fmt.Println(c) //[2,3,4]
二、切片
Go语言中,切片是长度可变、容量固定的相同的元素序列。Go语言的切片本质是一个数组。容量固定是因为数组的长度是固定的,切片的容量即隐藏数组的长度。长度可变指的是在数组长度的范围内可变。
1、切片的四种创建方式
- var slice1 = make([]int,5,10)
- var slice2 = make([]int,5)
- var slice3 = []int{}
- var slice4 = []int{1,2,3,4,5}
相对应的:
- slice1 := make([]int,5,10)
- slice2 := make([]int,5)
- slice3 := []int{}
- slice4 := []int{1,2,3,4,5}
以上对应的输出
[0 0 0 0 0]
[0 0 0 0 0]
[]
[1 2 3 4 5]
从3)、4)可见,创建切片跟创建数组唯一的区别在于 Type 前的“ [] ”中是否有数字,为空,则代表切片,否则则代表数组。因为切片是长度可变的
2、隐藏数组
Go的切片是在数组之上的抽象数据类型,所以创建的切片始终都有一个数组存在。
举例说明:
slice0 := []string{"a", "b", "c", "d", "e"}
slice1 := slice0[2 : len(slice0)-1]
slice2 := slice0[:3]
fmt.Println(slice0, slice1, slice2)
slice2[2] = "8"
fmt.Println(slice0, slice1, slice2)
输出:
[a b c d e] [c d] [a b c]
[a b 8 d e] [8 d] [a b 8]
也说明,切片slice0 、 slice1 和 slice2是同一个底层数组的引用,所以slice2改变了,其他两个都会变
3、append追加切片
内置函数append可以向一个切片后追加一个或多个同类型的其他值。如果追加的元素数量超过了原切片容量,那么最后返回的是一个全新数组中的全新切片。如果没有超过,那么最后返回的是原数组中的全新切片。无论如何,append对原切片无任何影响。
举例说明:
slice1 := make([]int, 2, 5)
fmt.Println(len(slice1), cap(slice1))
for k := range slice1{
fmt.Println(&slice1[k])
}
slice1 = append(slice1,4)
fmt.Println(len(slice1), cap(slice1))
for k := range slice1{
fmt.Println(&slice1[k])
}
slice1 = append(slice1,5,6,7)
fmt.Println(len(slice1), cap(slice1))
for k := range slice1{
fmt.Println(&slice1[k])
}
输出:
2 5 //长度和容量
0xc420012150
0xc420012158
3 5 //第一次追加,未超出容量,所以内存地址未发生改变
0xc420012150
0xc420012158
0xc420012160
6 10 //第二次追加,超过容量,内存地址都发生了改变,且容量也发生了改变,且是原来的2倍
0xc4200100f0
0xc4200100f8
0xc420010100
0xc420010108
0xc420010110
0xc420010118
再看一个例子:
slice1 := make([]int, 2, 5)
slice2 := slice1[:1]
fmt.Println(len(slice1), cap(slice1))
for k := range slice1{
fmt.Println(&slice1[k])
}
fmt.Println(len(slice2), cap(slice2))
for k := range slice2{
fmt.Println(&slice2[k])
}
slice2 = append(slice2,4,5,6,7,8)
fmt.Println(len(slice1), cap(slice1))
for k := range slice1{
fmt.Println(&slice1[k])
}
fmt.Println(len(slice2), cap(slice2))
for k := range slice2{
fmt.Println(&slice2[k])
}
以上输出:
2 5 //slice1的长度和容量
0xc4200700c0
0xc4200700c8
1 5 //slice2的长度和容量
0xc4200700c0
2 5 //slice2追加后,slice1的长度和容量、内存都未发生改变
0xc4200700c0
0xc4200700c8
6 10 //slice2追加后,超过了容量,所以slice2的长度和容量、内存地址都发生改变。
0xc42007e000
0xc42007e008
0xc42007e010
0xc42007e018
0xc42007e020
0xc42007e028
4、切片的复制 copy
复制切片要保证目标切片有足够长度(注意与容量无关),如果目标切片长度不足,则只复制到目标切片长度的数据,而不会自行改变目标切片长度来填充其它数据。
指针保存着一个值的内存地址,类型 *T代表指向T 类型值的指针。其零值为nil。
&操作符为它的操作数生成一个指针。
i := 42
p = &i
*操作符则会取出指针指向地址的值,这个操作也叫做“解引用”。
fmt.Println(*p) // 通过指针p读取存储的值
*p = 21 // 通过指针p设置p执行的内存地址存储的值
为什么需要指针类型呢?参考一个从go101网站上看到的例子 :
package main
import "fmt"
func double(x int) {
x += x
}
func main() {
var a = 3
double(a)
fmt.Println(a) // 3
}
在 double 函数里将 a 翻倍,但是例子中的函数却做不到。因为 Go 语言的函数传参都是值传递。double 函数里的 x 只是实参 a 的一个拷贝,在函数内部对 x 的操作不能反馈到实参 a。
把参数换成一个指针就可以解决这个问题了。
package main
import "fmt"
func double(x *int) {
*x += *x
x = nil
}
func main() {
var a = 3
double(&a)
fmt.Println(a) // 6
p := &a
double(p)
fmt.Println(a, p == nil) // 12 false
}
上面的程序乍一看你可能对下面这一行代码有些疑惑
x = nil
稍微思考一下上面说的Go语言里面参数都是值传递,你就会知道这一行代码根本不影响外面的变量 a。因为参数都是值传递,所以函数内的 x 也只是对 &a 的一个拷贝。
*x += *x
这一句把 x 指向的值(也就是 &a 指向的值,即变量 a)变为原来的 2 倍。但是对 x 本身(一个指针)的操作却不会影响外层的 a,所以在double函数内部的 x=nil 不会影响外面。
指针的限制
相较于 C 语言指针的灵活,Go 语言里指针多了不少限制,不过这让我们:既可以享受指针带来的便利,又避免了指针的危险性。下面就简单说一下 Go 对指针操作的一些限制
限制一:指针不能参与运算
来看一个简单的例子:
package main
import "fmt"
func main() {
a := 5
p := a
fmt.Println(p)
p = &a + 3
}
上面的代码将不能通过编译,会报编译错误:
invalid operation: &a + 3 (mismatched types *int and int)
也就是说 Go 不允许对指针进行数学运算。
限制二:不同类型的指针不允许相互转换。
下面的程序同样也不能编译成功:
package main
func main() {
var a int = 100
var f *float64
f = *float64(&a)
}
限制三:不同类型的指针不能比较和相互赋值
这条限制同上面的限制二,因为指针之间不能做类型转换,所以也没法使用==或者!=进行比较了,同样不同类型的指针变量相互之间不能赋值。比如下面这样,也是会报编译错误。
package main
func main() {
var a int = 100
var f *float64
f = &a
}
Go语言的指针是类型安全的,但它有很多限制,所以 Go 还有提供了可以进行类型转换的通用指针,这就是 unsafe 包提供的 unsafe.Pointer。在某些情况下,它会使代码更高效,当然如果掌握不好就使用,也更容易让程序崩溃。
unsafe 包
unsafe 包用于编译阶段可以绕过 Go 语言的类型系统,直接操作内存。例如,利用 unsafe 包操作一个结构体的未导出成员。unsafe 包让我可以直接读写内存的能力。
unsafe包只有两个类型,三个函数,但是功能很强大。
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
ArbitraryType是int的一个别名,在 Go 中ArbitraryType有特殊的意义。代表一个任意Go表达式类型。Pointer是int指针类型的一个别名,在 Go 中可以把任意指针类型转换成unsafe.Pointer类型。
三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。
Sizeof接受任意类型的值(表达式),返回其占用的字节数,这和c语言里面不同,c语言里面sizeof函数的参数是类型,而这里是一个值,比如一个变量。Offsetof:返回结构体成员在内存中的位置距离结构体起始处的字节数,所传参数必须是结构体的成员(结构体指针指向的地址就是结构体起始处的地址,即第一个成员的内存地址)。Alignof返回变量对齐字节数量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的值当作参数,具体细节咱们到以后聊内存对齐的文章里再说。
注意以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行
unsafe.Pointer
unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述:
- 任何类型的指针都可以被转化为Pointer
- Pointer可以被转化为任何类型的指针
- uintptr可以被转化为Pointer
- Pointer可以被转化为uintptr
unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void类型的指针),在Go 语言中是用于各种指针相互转换的桥梁,它可以持有任意类型变量的地址。
什么叫"可以持有任意类型变量的地址"呢?意思就是使用 unsafe.Pointer 转换的变量,该变量一定要是指针类型,否则编译会报错。
a := 1
b := unsafe.Pointer(a) //报错
b := unsafe.Pointer(&a) // 正确
和普通指针一样,unsafe.Pointer 指针也是可以比较的,并且支持和 nil 比较判断是否为空指针。
unsafe.Pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 unsafe.Pointer 类型。
// uintptr、unsafe.Pointer和普通指针之间的转换关系
uintptr <==> unsafe.Pointer <==> *T
uintptr
uintptr是 Go 语言的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是 uint64。
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
typedef unsigned long long int uint64;
typedef uint64 uintptr;
一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到uintptr类型的变量中(注:这个变量只是和当前指针有相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是随便将一个 uintptr 转为 unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。
还有一点要注意的是,uintptr 并没有指针的语义,意思就是存储 uintptr 值的内存地址在Go发生GC时会被回收。而 unsafe.Pointer 有指针语义,可以保护它不会被垃圾回收。