1、go基础
1.1、与其他语言对比
go语言与java有什么区别
语法和风格:
- Go语言的语法相对简洁,清晰易读,而是使用结构体和接口
- Java类和继承复杂
并发模型:
- Go语言轻量级线程和通信,并发更简单和高效。
- Java也有并发支持,但它使用线程和锁的模型,相对而言可能更复杂。
内存管理:
- Go语言具有垃圾回收机制,开发者无需手动管理内存。减少内存泄漏和提高开发效率。
- Java同样具有垃圾回收,但在某些情况下,可能需要更多的调优来处理大规模的、高性能的应用程序。
性能:
- golang比java快,
- go原生的编译性能生成的二进制文件相对较小
- java通常需要再java虚拟机JVM上运行
生态系统:
- 相对于一些其他主流语言,Go语言的第三方库数量可能相对较少。虽然Go社区在不断发展,但某些领域的库可能仍不如其他语言那样丰富。
错误处理
- 未使用到的会报错
- Go语言使用显式的错误处理机制,即通过返回值来传递错误。有时这会导致代码中充斥着处理错误的代码块,使得代码显得较为冗长。
go适合做什么
- 服务端开发
- 分布式系统,微服务
- 网络编程
- 区块链开发
- 内存KV数据库,例如boltDB、levelDB
- 云平台
1.2、符号
1.2.1、制表符
\t | 一个制表符 |
\r | 回车(与\n区别:从当前行最前面开始覆盖) |
1.2.2、格式化输出
%t | bool |
%b | 二进制 |
%o | 八进制fmt.Printf("%o\n", 255) // 输出:377 |
%O | fmt.Printf("%O\n", 255) // 输出:0o377 (Go 1.13+) |
%x | 十六进制表示,使用 a-f |
%X | 十六进制表示,使用 A-F |
%s | fmt.Printf("%s\n", "Hello, world!") // 输出:Hello, world! |
%q | fmt.Printf("%q\n", "Hello, world!") // 输出:"Hello, world!" |
%e | 科学计数法,如 -1234.456e+78 |
%E | 科学计数法,如 -1234.456E+78 |
%p | 指针地址,表示为十六进制,并加上前缀 0x |
%v | 按值的默认格式输出 |
%+v | fmt.Printf("%+v\n", struct{ X int }{X: 1}) // 输出:{X:1} |
%#v | fmt.Printf("%q\n", "Hello, world!") // 输出:"Hello, world!"/ 输出:struct { X int }{X:1} |
%T | 输出类型 |
1.3、数据类型
1.3.1、四种声明方式
var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)。
1.3.2、整数型
rune
处理中文、日文或者其他复合字符时,汉字3个字节
1.3.3、浮点
float32会出现小数后的尾数丢失
1.3.4、golang 中 make 和 new 的区别?
- 共同点:给变量分配内存
- 不同点:make:函数主要用于创建切片,map和通道
new:返回指向新分配的零值的指针,主要用于创建值类型(如结构体、数组等)的实例,但不会对这些实例进行初始化。
1.3.5、for range 的时候它的地址会发生变化么?
- for a,b := range c 遍历中, a 和 b 在内存中只会存在一份,每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,
- for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程。在每次循环时,创建一个临时变量。
1.4、值类型
- int,float,bool,string,数组和结构体
- 变量直接存储值,内存通常在栈中分配
1.4.1、常量
1.4.1.1、iota
示例 :自动计数
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
使用_跳过某些值
const (
n1 = iota //0
n2 //1
_
n4 //3
)
iota
声明中间插队
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
多个iota
定义在一行
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
1.4.2、结构体
- 结构体所有字段在内存中是连续的
反射
- 返回数据时大多数用的是json,而结构体中结构体在其他包,引用时首字母需要大写,现在就需要自定返回`json:"name"`就可以将当前字段别名化
type person struct[
Name string `json:"name"`
}
golang 中解析 tag 是怎么实现的?反射原理是什么?
- 反射机制允许在运行时检查类型信息、获取和修改变量的值、调用方法等。反射的基本思想是在运行时检查变量的类型信息
- 反射的核心是
reflect
包,其中的Type
和Value
类型分别提供了类型信息和值信息。
结构体之间转换
两个结构体里字段要完全一样,赋值方式A(b)不可以a = b
package main
import "fmt"
type A struct {
a int
}
type B struct {
a int
}
func main() {
var a A
var b B
b.a = 1
a = A(b)
fmt.Println(a)
}
1.4.3、字符串
- 字符串的内容不能在初始化后被修改,但string底层是[]byte,转成[]byte可以进行修改
字符串转换
转成字符串
strconv.FormatInt(int,10) strconv.Itoa(int) |
整数转为10进制字符串 |
strconv.FormatFloat(float,’f‘,10,64) | 浮点数,格式,保留小数位数10,float64 |
strconv.FormatBool(bool) | bool转string |
字符串转出
- strconv.ParseBool("true")
- strconv.ParseInt("99",10,0) 注string,10进制,第3位 0:int, 8 :int8, 16 : int16……
- strconv.ParseUint("99",10,0)
- strconv.ParseFloat("") string,大小
1.4.3、Go 语言中不同的类型如何比较是否相等?
- string,int,float interface 等可以通过 reflect.DeepEqual 和等于号进行比较
- slice,struct,map 使用 reflect.DeepEqual 来检测是否相等
1.5、引用类型
- 变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由Gc来回收。
1.5.1、slice
1.5.1.1、slice操作
- copy:相当于覆盖
- append:在原数组后面添加数据
1.5.1.2、数组和切片的区别
-
长度:
- 数组的长度是固定的,在声明时需要指定长度,并且不能改变。
- 切片的长度是可变的,可以根据需要动态增长或缩减。
-
声明方式:
- 数组的声明方式为
[长度]类型
,例如[3]int
表示包含 3 个整数的数组。 - 切片的声明方式为
[]类型
,例如[]int
表示一个整数切片。
- 数组的声明方式为
-
初始化:
- 数组可以通过初始化列表进行初始化,例如
[3]int{1, 2, 3}
。 - 切片通常使用
make()
函数或者直接声明并初始化来进行初始化,例如make([]int, 3)
或者[]int{1, 2, 3}
。
- 数组可以通过初始化列表进行初始化,例如
-
传递方式:
- 数组在函数调用时会进行值拷贝,即传递的是数组的副本。
- 切片在函数调用时传递的是切片的引用,即底层共享相同的底层数组。
-
长度和容量:
- 切片除了长度外,还有一个容量(Capacity)的概念。长度表示切片当前包含的元素个数,而容量则表示底层数组从切片开始位置到底层数组末尾的元素个数。
- 使用内置的
len()
和cap()
函数可以分别获取切片的长度和容量。
-
操作:
- 数组是一个连续的内存块,因此支持常量时间的索引访问和迭代操作。
- 切片支持动态增长和缩减、追加、拷贝等操作,因为切片底层是一个指向数组的指针、长度和容量的组合。
1.5.1.3、Go 的 slice 底层数据结构和一些特性
- 切片底层是一个指向数组的指针、长度和容量的组合
- len 表示切片长度,cap 表示切片容量。
- 当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,返回新的 slice。
- `slice` 的长度小于 1024,它的容量翻倍;如果长度大于等于 1024,它的容量增加 25%
type slice struct{
ptr *[2]int
len int
cap int
}
1.5.1.4、从数组中取一个相同大小的slice有成本吗?
- [:]从切片中取数组并不会有明显的额外成本,它只是创建了一个新的切片对象,其长度和容量与原始切片相同。这个操作的时间复杂度是 O(1)。
- 用切片语法从数组中取一个切片时,实际上并没有进行底层数组的复制,而是创建了一个新的切片对象,该切片对象与原始数组共享相同的底层数组。
1.5.1.5、切片是否线程安全,如何保证安全
- 不安全
- 使用互斥锁
- 使用通道
1.5.1.6、切片是否会自动进行内存释放?为什么?
- 不进行内存的分配和释放
- 数组的存储是由 Go 的垃圾回收器进行管理的。当一个对象(包括底层数组)不再被引用时,垃圾回收器将释放其占用的内存。
1.5.1.7、切片如何避免切片引起的内存泄漏
- 垃圾回收机制,开发者相对不太容易发生严重的内存泄漏问题。然而,通过良好的代码实践,可以更进一步减少不必要的内存占用,确保程序的性能和稳定性。
- 避免循环引用:确保切片没有形成循环引用,即使切片中的元素不再需要,也能及时释放内存。
1.5.2、map
1.5.2.1、map操作
- delete(myMap, "Bob") 删除
1.5.2.2、map 使用注意的点,是否并发安全?
map
(key)必须是可比较的类型。这包括基本数据类型(如整数、浮点数、字符串、布尔值)和某些复合类型(如指针、数组、结构体)- 如果想在
map
中使用结构体作为键,你需要确保结构体的字段都是可比较的类型。换句话说,结构体中的字段不能包含切片、映射或函数等不可比较的类型。 - 想要保证遍历map时元素有序,可以使用辅助的数据结构,例如orderedmap。
- 要先初始化,否则panic
- 不安全,确保安全可以采取:互斥锁
- sync.Map: Go语言提供了`sync`包中的`Map`类型,它是一种并发安全的 map 实现。它使用了一种更加复杂的内部数据结构来支持并发访问而不需要额外的锁。
1.5.2.3、map 中删除一个 key,它的内存会释放么?
- 删除`map`中的一个键值对并不会直接释放相应的内存。Go的垃圾回收器负责管理内存,而不是在每次删除`map`的键值对时立即释放内存。
- 垃圾回收器会定期扫描不再被引用的内存块,并在需要时将其释放。
1.5.2.4、子主题 nil map 和空 map 有何不同?
- `nil` map 是指未初始化的 map(零值)
- 空 map 是一个已经初始化但没有键值对的 map。
1.5.2.5、map 的数据结构是什么?
- 底层是哈希表,通过计算找到对应位置,每个位置有对应的桶,底层用链表来解决冲突 ,出现冲突时一个 bmap 可以放 8 个 kv,桶用链表连接。
1.5.2.6、扩容
- 等量扩容:并不是扩大容量,buckets数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次。桶内溢出桶数量大于等于2^hash数组长度,长度最大取15,达到长度等量扩容
- 增量扩容:桶内key-v总数/hash数组长度>6.5触发扩容,负载因子(总数/桶数组长度) > 6.5时,桶数组两倍增长。
1.5.2.7、数据迁移
- 逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
- 当第8个键值对插入时,将会触发扩容,数据搬迁过程中,原bucket中的键值对将存在于新bucket的前面,新插入的键值对将存在于新bucket的后面。
1.5.2.8、map数据结构
type hmap struct {
count int // 元素个数
flags uint8 // 状态标志
B uint8 // 初始容量的对数(2^B)
noverflow uint16 // 溢出桶的个数
hash0 uint32 // hash种子
buckets unsafe.Pointer // 桶数组的指针
oldbuckets unsafe.Pointer // 旧桶数组的指针
nevacuate uintptr // 正在扩容时当前已迁移的桶数
extra *mapextra // 存储额外信息的指针
}
1.5.2.8、map底层增删改查
初始化
插入
- 判断是否初始化,没有panic
- 其他线程是否在写入
- 产生一个标识标识正在写操作
- 根据key值算出哈希值
- 取哈希值低位与hmap.B取模确定bucket位置
- 判断是否需要进行扩容,分担扩容压力
- 查找该key是否已经存在,如果存在则直接更新值
- 如果没找到将key,将key插入
- 清除之前的正在写标记
查看
- 判断map是否初始化,如果没有或数量为0返回0值(为空)
- 是否有其他线程并发写入map,是报错
- key算处hash值
- 对桶取模找到桶位置
- 遍历桶链表
- 有返v无返回零
删除
- emptyone 代表当前凹槽数据不存在
- emptyRest 代表当前凹槽到结尾都没有数据存在
- 判断是否初始化,没有panic
- 其他线程是否在写入
- 产生一个标识标识正在写操作
- 判断是否在扩容,如果存在扩容需要承担一部分数据迁移工作
- 删除数据后首先将当前空间标记为emptyone,如果删除数据再桶的末尾,标记为Rest会想前查找上一数据是否为空如果是空,会将上一数据标记为emptyRest
1.5.2.8、存储数量
- 底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。
- 装载因子=填入表中的元素个数/散列表的长度
- 装载因子越大,说明空闲位置越少,冲突越多,散列表的性能