文章目录
数组和切片区别?
数组是固定长度的, 大小不可改变, 数组通过值传递
切片是可变长度, 切片是轻量级数据结构, 三个属性, 指针、长度、容量
不需要指定大小切片是地址传递, 可以通过数组来初始化, 也可以通过
内置函数make来初始化, 初始化时候, len=cap, 然后进行扩容。
go语言中值传递和地址传递区别?
1.值传递只会把参数的值复制一份放进对应函数, 两个变量地址不同。
不可相互修改。
2地址传递会将变量本身传入对应函数, 在函数中可以对变量进行内容修改。
defer作用特点:
defer作用, defer后面的语句会延迟执行, 直到包含该defer语句的函数执行完毕
defer后面的函数才会被执行, defer语句的函数是通过return正常结束,
不论包含defer语句的函数是通过return正常结束还是由于panic导致的异常结束。
可以在一个函数中执行多条defer语句, 他们执行顺序与声明顺序相反。
可以在一个函数中执行多条defer语句, 他们的执行顺序与声明的顺序相反。
切片本身不是动态数组或者数组指针, 内部是
切片是有3个字段的数据结构
1.指向底层数组指针
2.切片大小
3.切片容量
slice的扩容机制?
当小于1024的时候, 每次扩容动作都是之前的一倍。
当大于1024的时候, 每次扩容1/4;
go中map的内部原理
常用解决散列冲突方法是:
1.开放寻址法
2.线性探测法
3.链地址法。
go中的map使用改进后的链表法解决冲突, 在map中有几个重要
属性。
count:表示当前哈希表中的元素数量
B表示当前buckets数量一般用2^B表示
Hash0:表示随机种子。
Buckets是一个指针,保存bmap的位置, bmp是具体保存
key value的数据结构,
tophash保存hash值的高八位, 用来匹配key值。
key和value数组分别用来保存具体需要保存的数据。
overflow是溢出标志, 当前如果数组已经存满, 会再开辟一个新的bmap
map主要是由hmap和bmap来组成
初始化:
一个桶默认存放8个键值对。
标准桶和溢出桶。
写入数据:
通过标号可以确定索引的位置。
根据低B位确定具体通的位置。
tophash实际上存储的是hash值的高8位, 后面比较的时候,
实际上会比较高8位这个。
读取数据:
map的扩容:
等量扩容:原来4个桶, 扩容后还是4个桶,
迁移动作:
hash值通过低B位确定具体放到哪个桶里面, 高8位去桶中的key值。
go中sync.map原理
sync.Map是Go的一种并发安全的map类型。在Go 1.9版本中被引入,主要用于解决多个协程(goroutine)并发读/写的线程安全问题。它的内部实现包括了一些复杂的逻辑来优化性能,主要分为以下几个方面:
双Map设计:sync.Map内部维护了两个map:read和dirty。read Map用于读取时用,并且有意设计为原子操作无锁的读取。所有的读操作都优先搜索read Map。dirty Map存储的则是所有待更新的键值对以及read Map中不存在的键值对。
读写分离:在更新元素时,并不直接在read Map上进行,而是加锁后在dirty Map中更新。当read Map读取不到时(即读"miss"),再从dirty Map中读取。每次从dirty中读取元素后,将"miss"的计数+1。
动态调整:dirty Map在首次创建或者新增元素后,当"miss"的计数大于等于dirty Map的长度时,就会把dirty Map整个覆盖(提升)到read Map。这样就保证了大部分的读取都能在read Map中命中。
延迟删除:对Map元素的删除,并非直接删除,而是先进行标记,只有在将dirty Map提升为read Map时,才会做真正的删除操作,这是为了确保在删除操作发生时,不会影响到read Map的读取操作。
sync.Map并不是在所有情况下都比内建的map类型更好,它是为一些特定场景设计的,比如在大部分操作为读取,且键值对的写入次数比较少或者稳定的情况下,sync.Map的性能较好。如果键值对的写入较为频繁,使用内建的map加上互斥锁反而可能会有更好的性能。
go中gc算法
一共分为四个阶段:
- 栈扫描(开始时STW)
- 第一次标记(并发)
- 第二次标记(STW)
- 清除(并发)
整个进程空间里申请每个对象占据的内存可以视为一个图, 初始状态下每个内存对象都是白色标记。
- 先STW,做一些准备工作,比如 enable write barrier。然后取消STW,将扫描任务作为多个并发的goroutine立即入队给调度器,进而被CPU处理
- 第一轮先扫描root对象,包括全局指针和 goroutine 栈上的指针,标记为灰色放入队列
- 第二轮将第一步队列中的对象引用的对象置为灰色加入队列,一个对象引用的所有对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象;
- 第三轮再次STW,将第二轮过程中新增对象申请的内存进行标记(灰色),这里使用了write barrier(写屏障)去记录
Go语言的垃圾回收(Garbage Collection,简称GC)是一种自动化的内存管理机制,用于回收不再使用的内存,减轻开发者对内存管理的负担。Go语言的GC流程主要包括以下几个阶段:
1. **标记阶段(Marking):**
- GC从根对象(Root Object)开始,根对象包括活动的Goroutines的栈上对象、全局变量以及正在使用的临时对象等。
- 通过根对象,GC遍历整个对象图(Object Graph),将所有可达的对象标记为活动对象。即将活动对象打上标记。
2. **清除阶段(Sweeping):**
- 在清除阶段,GC遍历整个堆空间,清理未被标记的对象。
- 未被标记的对象被视为不再使用,GC会将这些未被标记的对象进行回收,释放其占用的内存。
3. **回收阶段(Reclaiming):**
- 在回收阶段,GC将已清理的内存重新纳入空闲内存池中,以供后续的内存分配使用。
4. **压缩阶段(Compaction,可选):**
- 在压缩阶段,GC会尝试将剩余的内存片段紧凑在一起,减少内存碎片化,提高内存使用效率。
值得注意的是,Go语言的GC是并发执行的,即垃圾回收器会在后台与应用程序并发运行,而不会停止整个程序的执行。Go的垃圾回收器采用了三色标记算法和并发标记清除(Concurrent Mark and Sweep)策略,以提高GC的效率和性能。Go的GC设计力求在减少停顿时间和系统负担的同时,保持高性能和可伸缩性。
什么是goroutine泄漏?如何避免goroutine泄漏?
Goroutine泄漏指的是在Go程序中,无法正确地释放已经创建的Goroutine,导致这些Goroutine无法被回收,最终占用了过多的资源而不能被垃圾回收。这种情况可能导致程序的性能下降、资源耗尽,甚至引发严重的问题,例如内存泄漏等。
Goroutine泄漏通常发生在以下情况下:
-
忘记等待Goroutine完成: 当启动一个新的Goroutine并且没有正确等待它完成时,这个Goroutine可能会在后台一直运行,无法被回收。例如,没有使用
sync.WaitGroup
等待Goroutine完成或没有使用通道通知Goroutine退出。 -
无限循环: 如果Goroutine在无限循环中运行,并且没有适当的退出条件,它将永远不会退出,也无法被回收。
为了避免Goroutine泄漏,可以采取以下措施:
-
使用sync.WaitGroup: 在启动Goroutine时,使用
sync.WaitGroup
来等待Goroutine完成。这样可以确保在主函数退出之前,所有Goroutine都已经完成了任务。 -
使用通道通知退出: 如果Goroutine需要长时间运行,应该使用通道来通知它退出。在Goroutine中定期检查通道,如果接收到退出信号,及时退出Goroutine。
-
设置合适的超时: 如果Goroutine可能因为某些原因无法正常退出,可以在主函数中设置一个超时,确保程序不会无限等待。
-
避免无限循环: 在Goroutine中,确保有适当的退出条件,避免无限循环导致Goroutine无法退出。
总之,正确管理Goroutine的生命周期非常重要,以避免Goroutine泄漏和资源浪费。确保在创建Goroutine时适当等待其完成或通知其退出,并确保有适当的退出条件,以避免无限循环。这样可以保持程序的性能和资源利用率,并确保Go程序的稳定运行。
如何使用pprof进行性能分析?
使用pprof进行性能分析可以帮助我们找到程序中的性能瓶颈和资源消耗较高的地方,从而进行优化。在Go语言中,pprof是Go标准库自带的性能分析工具,可以通过以下步骤使用pprof进行性能分析:
-
导入pprof包:
在Go程序的导入部分添加pprof包的导入语句:import _ "net/http/pprof"
。这样在程序运行时,pprof就会注册相关的HTTP路由,以便我们通过HTTP请求来访问性能分析数据。 -
启动HTTP服务器:
在程序的适当位置,添加启动HTTP服务器的代码。可以通过调用http.ListenAndServe()
来启动一个HTTP服务器。例如,可以在main()
函数中添加以下代码:go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()
-
生成性能数据:
在程序运行过程中,我们可以通过访问http://localhost:6060/debug/pprof/
来生成性能数据。例如,可以使用浏览器或curl访问http://localhost:6060/debug/pprof/profile
来生成CPU分析数据。 -
分析性能数据:
生成的性能数据将会以不同的形式提供给我们。我们可以使用go tool pprof
命令来分析这些数据。例如,可以使用以下命令来查看CPU分析数据:go tool pprof http://localhost:6060/debug/pprof/profile
-
查看性能报告:
执行上述命令后,会进入交互式的pprof命令行界面。在pprof命令行中,可以输入top
命令来查看性能报告,列出CPU消耗最多的函数。 -
退出pprof命令行:
在pprof命令行界面中,可以输入quit
命令来退出。
通过上述步骤,我们可以使用pprof对Go程序进行性能分析,并找到性能瓶颈和资源消耗较高的地方,从而进行针对性的优化。
go中的内存布局
Go语言的内存布局可以分为以下几个部分:
-
栈(Stack):用于存放函数的局部变量、参数以及函数调用时的上下文信息。栈是一种高效的内存分配方式,它的大小是固定的,并且随着函数的调用和返回而动态增减。
-
堆(Heap):用于动态分配内存,
主要
用于存放程序运行时动态创建的对象。在Go语言中,通过new
和make
关键字来进行堆上内存的分配。 -
数据区(Data Segment):用于存放全局变量、静态变量以及常量。这部分内存在程序启动时就已经被分配,直到程序结束时才释放。
-
代码区(Code Segment):用于存放程序的可执行代码。这部分内存通常是只读的,因为代码在程序执行过程中不应该被修改。
Go语言的内存管理由运行时系统(runtime)负责,它包含了垃圾回收器(Garbage Collector)来自动管理不再使用的内存。这样,开发者可以专注于业务逻辑而不用手动管理内存,使得Go语言在编写安全且高效的并发程序时更加便捷。
go中context的作用和使用示例
在Go语言中,context
包的作用是传递请求的上下文信息,并且可以用于控制多个 Goroutine 之间的取消、超时和截止时间。它在处理请求的多个 Goroutine 中传递请求相关的信息,例如请求的截止时间、取消信号等。context
包提供了一种便捷的方式来管理这些上下文信息,避免在程序中传递大量的参数。
context
包提供了Context
类型和相关函数,它们包括:
-
context.Background()
:创建一个空的Context
,一般用于程序的入口,作为顶层的Context
,不能被取消。 -
context.TODO()
:创建一个空的Context
,类似于Background()
,但用于表示还未确定的Context
。 -
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
:创建一个可取消的Context
,并返回一个用于取消的函数CancelFunc
。当调用该函数时,所有衍生自该Context
的Context
也会被取消。 -
context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
:创建一个带有截止时间的Context
,当截止时间到达后,该Context
会自动被取消。 -
context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
:创建一个带有超时时间的Context
,当超时时间到达后,该Context
会自动被取消。 -
context.WithValue(parent Context, key interface{}, val interface{}) Context
:创建一个带有键值对的Context
,可以用于在Context
中传递请求相关的值。
以下是一个简单的context
使用示例:
package main
import (
"context"
"fmt"
"time"
)
func process(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("Process completed.")
case <-ctx.Done():
fmt.Println("Process cancelled.")
}
}
func main() {
// 创建一个带有超时时间的Context,超时时间为5秒
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // 在函数结束时调用cancel()来释放资源
// 启动一个Goroutine来处理任务
go process(ctx)
// 主Goroutine等待一段时间
time.Sleep(10 * time.Second)
}
在上面的示例中,主函数创建了一个带有5秒超时时间的Context
,然后启动了一个 Goroutine 来执行process
函数。在process
函数中,使用select
语句同时监听任务的完成和Context
的取消信号。当超时时间到达时,Context
会自动取消,进而导致process
函数中的<-ctx.Done()
分支被执行,输出"Process cancelled."。
需要注意的是,当我们主动调用cancel()
函数时,也会触发Context
的取消,因此在实际使用中,我们应该根据实际情况选择是使用超时时间取消还是手动取消。
go中select作用
在Go语言中,select
是一种用于处理多个通道操作的控制结构。select
语句用于在多个通道之间进行选择,从而实现非阻塞地进行通信或处理多路复用的场景。
select
的作用如下:
-
监听多个通道:
select
可以同时监听多个通道,当其中任意一个通道可以进行读写操作时,select
语句就会选择并执行该分支的代码块。 -
避免阻塞:
select
语句可以避免在通信操作中出现阻塞。如果所有通道都没有准备好(没有数据可读或者无法写入数据),select
语句会等待直到有一个通道准备好,然后执行该分支。 -
非阻塞地进行通信:通过
default
分支,可以在没有任何通道准备好的情况下,立即执行某些默认操作,从而实现非阻塞的通信。
示例:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch1 <- 42
}()
go func() {
time.Sleep(3 * time.Second)
ch2 <- "Hello, World!"
}()
select {
case num := <-ch1:
fmt.Println("Received from ch1:", num)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
case <-time.After(4 * time.Second):
fmt.Println("Timeout: no channel was ready.")
}
}
在这个示例中,我们创建了两个通道ch1
和ch2
,并在两个Goroutine中分别向它们发送数据。通过select
语句,我们同时监听两个通道的操作,并等待最先准备好的通道。在这里,ch1
的数据会先准备好,所以select
会执行第一个分支,并输出"Received from ch1: 42"。如果ch1
的数据没有在指定时间内准备好,select
会执行time.After
分支,并输出"Timeout: no channel was ready."。
总结来说,select
是Go语言中一种非常实用的控制结构,它使得在并发编程中处理多个通道操作变得更加灵活和高效。