文章目录
Golang基础知识
1.make和new的区别:
new:用来初始化一个对象,并且返回该对象的首地址.其自身是一个指针.可用于初始化任何类型
make:返回一个初始化的实例,返回的是一个实例,而不是指针,其只能用来初始化:slice,map和channel三种类型
package main
import (
"fmt"
)
func main() {
a := new([]int)
fmt.Println(a) //输出&[],a本身是一个地址
b := make([]int, 1)
fmt.Println(b) //输出[0],b本身是一个slice对象,其内容默认为0
}
2.数组
- 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;
- 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;(当数组中元素的个数大于4时,在编译期间就会在静态区初始化好数组,在运行时将数组内存拷贝到栈上。这两个过程相比,初始化耗费的时间较多,拷贝耗费的时间少,将初始化过程放置在编译期间而不是运行期间能够提升程序运行时的效率。)
- 无论是在栈上还是静态存储区,数组在内存中都是一连串的内存空间,我们通过指向数组开头的指针、元素的数量以及元素类型占的空间大小表示数组。
3.切片
type SliceHeader struct {
Data uintptr // 指向数组的指针
Len int // 当前切片的长度
Cap int // 当前切片的容量,即 `Data` 数组的大小
}
append的时候发生扩容的动作
-
append单个元素,或者append少量的多个元素,这里的少量指double之后的容量能容纳,这样就会走以下扩容流程,不足1024,双倍扩容,超过1024的,1.25倍扩容。
-
若是append多个元素,且double后的容量不能容纳,直接使用预估的容量。
(还需要内存对齐)
敲重点!!!!此外,以上两个分支得到新容量后,均需要根据slice的类型size,算出新的容量所需的内存情况capmem
,然后再进行capmem
向上取整,得到新的所需内存,除上类型size,得到真正的最终容量,作为新的slice的容量。
4.哈希
Go 语言使用拉链法来解决哈希碰撞的问题实现了哈希表,它的访问、写入和删除等操作都在编译期间转换成了运行时的函数或者方法。哈希在每一个桶中存储键对应哈希的前 8 位,当对哈希进行操作时,这些 tophash
就成为可以帮助哈希快速遍历桶中元素的缓存。
哈希表的每个桶都只能存储 8 个键值对,一旦当前哈希的某个桶超出 8 个,新的键值对就会存储到哈希的溢出桶中。随着键值对数量的增加,溢出桶的数量和哈希的装载因子也会逐渐升高,超过一定范围就会触发扩容,扩容会将桶的数量翻倍,元素再分配的过程也是在调用写操作时增量进行的,不会造成性能的瞬时巨大抖动。
- map为什么不稳定?
查看资料后,我才发现在go 1之前,两次遍历是相同的,但是为什么官方要改掉呢?
其实是为了安全性和稳定性,go开发组发现有些程序员已经开始依赖遍历顺序稳定(不是有序)这个特性来开发程序,这其实并不好,因为这个“稳定”因平台不同而不同,在 win 上可能是 1342,但是在linux上可能就是4312。这会导致程序不可移植,因此go要求开发者自己实现稳定有序的遍历。
go语言会提前取一个随机数,把桶的遍历顺序随机化
5.字符串
实际上是一片连续的内存空间,我们也可以将它理解成一个由字符组成的数组
如果是代码中存在的字符串,编译器会将其标记成只读数据 SRODATA
,假设我们有以下代码,其中包含了一个字符串,当我们将这段代码编译成汇编语言时,就能够看到 hello
字符串有一个 SRODATA
的标记:
$ cat main.go
package main
func main() {
str := "hello"
println([]byte(str))
}
$ GOOS=linux GOARCH=amd64 go tool compile -S main.go
...
go.string."hello" SRODATA dupok size=5
0x0000 68 65 6c 6c 6f hello
...
只读只意味着字符串会分配到只读的内存空间,但是 Go 语言只是不支持直接修改 string
类型变量的内存空间,我们仍然可以通过在 string
和 []byte
类型之间反复转换实现修改这一目的:
- 先将这段内存拷贝到堆或者栈上;
- 将变量的类型转换成
[]byte
后并修改字节数据; - 将修改后的字节数组转换回
string
;
字符串在 Go 语言中的接口其实非常简单,每一个字符串在运行时都会使用如下的 reflect.StringHeader
表示,其中包含指向字节数组的指针和数组的大小:
type StringHeader struct {
Data uintptr
Len int
}
与切片的结构体相比,字符串只少了一个表示容量的 Cap
字段,而正是因为切片在 Go 语言的运行时表示与字符串高度相似,所以我们经常会说字符串是一个只读的切片类型。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
因为字符串作为只读的类型,我们并不会直接向字符串直接追加元素改变其本身的内存空间,所有在字符串上的写入操作都是通过拷贝实现的。
6.函数调用 #
无论是传递基本类型、结构体还是指针,都会对传递的参数进行拷贝
-
传递结构体时:会拷贝结构体中的全部内容;
-
传递结构体指针时:会拷贝结构体指针(指针作为参数传入某个函数时,函数内部会复制指针,也就是会同时出现两个指针指向原有的内存空间,所以 Go 语言中传指针也是传值)
结构体在内存中是一片连续的空间,指向结构体的指针也是指向这个结构体的首地址
Go 语言在传递参数时使用了传值的方式,接收方收到参数时会对这些参数进行复制;了解到这一点之后,在传递数组或者内存占用非常大的结构体时,我们应该尽量使用指针作为参数类型来避免发生数据拷贝进而影响性能
Go 通过栈传递函数的参数和返回值,在调用函数之前会在栈上为返回值分配合适的内存空间,随后将入参从右到左按顺序压栈并拷贝参数,返回值会被存储到调用方预留好的栈空间上,我们可以简单总结出以下几条规则:
-
通过堆栈传递参数,入栈的顺序是从右到左,而参数的计算是从左到右;
-
函数返回值通过堆栈传递并由调用者预先分配内存空间;
-
调用函数时都是传值,接收方会对入参进行复制再计算;