goroutine 调度的本质:就是将 Goroutine 按照一定算法放到 CPU 上去执行。
在 Go 中,协程是轻量级的用户级线程,它们不直接运行在操作系统的线程上。Go 使用了一种特殊的调度器(Schedule),负责管理和分配协程到操作系统线程,以便并发执行。
补充:Go 的调度器具有自动的伸缩性,可以根据需要创建和销毁操作系统线程,以适应应用程序的并发需求。这使得 Go 能够高效的处理大量的协程,而不会浪费过多的系统资源。
总结:Go 的协程和调度器使得并发编程更容器,同时也更高效,因为它们隐藏了许多操作系统级线程管理的复杂性,使开发者能够专注于应用程序逻辑。
Context(上下文)是Golang应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
几个实现context接口的对象:
context 机制允许在协程之间传递控制信号和请求范围值,以及安全地取消操作。这对于管理并发操作和资源的生命周期非常有用。context 的工作原理是基于 Go 语言的并发特性和协程的能力,以及调度器的支持来实现的。
竞态条件是多个协程访问共享数据时可能导致的问题。
使用 go build、go run、go test 命令时,添加 -race
标识可以检查代码中是否存在资源竞争。
竞态(Race Condition):竞态是一种并发编程中的问题,它发生在多个协程(goroutine)试图同时访问和修改共享数据时。这可能导致不可预测的行为和数据不一致。竞态条件的存在通常是因为缺乏适当的同步机制。
例子: 假设有两个协程同时尝试增加一个变量的值:在这个例子中,两个协程同时修改 count 变量,由于没有适当的同步,最终的结果可能是不确定的,可能不是预期的 2000。这就是竞态条件。
package main
import (
"fmt"
"sync"
)
var count int
var wg sync.WaitGroup
func increment() {
for i := 0; i < 1000; i++ {
count++
}
wg.Done()
}
func main() {
wg.Add(2)
go increment()
go increment()
wg.Wait()
fmt.Println("Count:", count)
}
解决竞态问题:
使用互斥锁(Mutex): 互斥锁是最常用的解决竞态问题的方法。它允许只有一个协程同时访问临界区(共享数据),从而避免竞态条件。在 Go 中,你可以使用 sync.Mutex 来创建互斥锁。
var mutex sync.Mutex
func increment() {
mutex.Lock()
count++
mutex.Unlock()
}
使用通道(Channel): 通道可以用来安全地在协程之间传递数据,避免竞态问题。通过发送和接收数据,你可以确保只有一个协程能够修改共享数据。
var ch = make(chan int)
func increment() {
ch <- 1
count++
<-ch
}
使用原子操作: Go 提供了原子操作的支持,例如 sync/atomic 包中的函数,这些函数可以在不需要互斥锁的情况下执行原子操作。
import "sync/atomic"
var count int32
func increment() {
atomic.AddInt32(&count, 1)
}
内存逃逸是编译器将变量分配到堆上而不是栈上的情况。
编译时可以借助选项 -gcflags=-m
,查看变量逃逸的情况
内存逃逸(Memory Escape):内存逃逸发生在编译器无法确定一个变量的生命周期时,它可能会分配到堆上而不是栈上。这可能会导致性能问题,因为堆上的内存分配和释放开销较大。
例子: 在下面的示例中,x 变量的生命周期超出了函数的范围,因此编译器将它分配到了堆上:这里的 x 变量的内存分配发生在堆上,因为它的生命周期无法在编译时确定。这可能会导致不必要的堆分配和额外的垃圾收集负担,对性能造成影响。
package main
func create() *int {
x := 10
return &x // 返回局部变量的指针
}
func main() {
y := create()
// 在这里,编译器无法确定 x 变量何时结束生命周期
// 所以 y 指向的数据分配在堆上
}
解决内存逃逸问题:
为了能让CPU可以更快的存取到各个字段,Go编译器会帮你把struct结构体做数据的对齐。所谓的数据对齐,是指内存地址是所存储数据大小(按字节为单位)的整数倍,以便CPU可以一次将该数据从内存中读取出来。编译器通过在结构体的各个字段之间填充一些空白已达到对齐的目的。
不同硬件平台占用的大小和对齐值都可能是不一样的,每个特定平台上的编译器都有自己的默认“对齐系数”,32位系统对齐系数是4,64位系统对齐系数是8
不同类型的对齐系数也可能不一样,使用Go 语言中的unsafe.Alignof函数可以返回相应类型的对齐系数,对齐系数都符合2^n这个规律,最大也不会超过8
对齐原则:
type T2 struct{
i8 int8
i64 int64
i32 int32
}
type T3 struct{
i8 int8
i32 int32
i64 int64
}
type C struct {
a struct{}
b int64
c int64
}
type D struct {
a int64
b struct{}
c int64
}
type E struct {
a int64
b int64
c struct{}
}
type F struct {
a int32
b int32
c struct{}
}
func main() {
// 使用 Go 语言的 unsafe.Sizeof 函数来获取特定类型的大小
fmt.Println(unsafe.Sizeof(C{})) // 16
fmt.Println(unsafe.Sizeof(D{})) // 16
fmt.Println(unsafe.Sizeof(E{})) // 24
fmt.Println(unsafe.Sizeof(F{})) // 12
}
在 Go 语言中,new 和 make 是两个用于分配内存的内建函数,它们用于创建不同类型的对象。它们的主要区别在于用途和返回值类型。
var i *int
i = new(int) // 创建一个 int 类型的指针,i 指向的值为 0
slice := make([]int, 5) // 创建一个包含 5 个整数的切片,每个元素为 0
Go 中的切片(slice)是一种灵活的数据结构,其实现原理基于数组。切片本质上是一个包装了数组的结构,它引用数组的一部分,可以动态调整大小。下面是切片的主要实现原理:
总的来说,切片是一种非常方便的数据结构,它允许你有效地处理变长的数据,同时避免了手动管理内存。在需要动态调整大小的情况下,切片是一种非常强大的工具,因为它自动处理了底层数数组的复制和管理。
type slice struct{
array unsafe.Pointer
len int
cap int
}
golang 中的 map 是一个指针,占用 8 个字节,指向 hmap 结构体,map 底层是基于哈希表 + 链表地址法 存储的。
map 的特点:
实现细节:
总结:Go 中的 map 使用哈希表来实现键值存储,它具有自动扩容功能,能够快速查找和插入键值对。但是 map 不保证元素的顺序。在多线程环境下,map 不是线程安全的,需要进行适当的同步操作来避免竞态条件。如果需要线程安全的映射,可以使用 sycn.Map
类型。
尽管map是无序的,但如果需要按照某种特定顺序访问map中的元素,可以通过将键存储在切片中,并对切片进行排序来实现。
什么是负载因子?
负载因子的选择是一种权衡,它涉及到性能和内存使用之间的取舍。以下是一些背后的原因:
根据这份测试结果和讨论,Go官方取了一个相对适中的值,把Go中的 map的负载因子硬编码为6.5,这就是6.5的选择缘由。
这意味着在Go语言中,当map存储的元素个数大于或等于6.5*桶个数时,就会触发扩容行为。
扩容时机:在向 map 插入新 Key 的时候,会进行条件检测,符合以下 2 个条件,就会触发扩容。
扩容条件:
对于条件2,其实算是对条件1的补充。因为在负载因子比较小的情况下,有可能 map 的查找和插入效率也很低,而第1点识别不出来这种情况。
表面现在就是负载因子比较小,即 map 中元素总数少,但是桶数量多(真实分配的桶数量多,包括大量的溢出桶)。比如不断的增删,这样会造成 overflow 的 bucket 数量增多,但是负载因子又不高,达不到第1点的临界值,就不能触发扩容来缓解这种情况。这样会造成桶的使用率不高,值存储得比较稀疏,查找插入效率会变得非常低,因此有了第2个扩容条件。
扩容机制:
sync.Map 是 Go 语言标准库中提供的一种线程安全的键值存储数据结构,它用于在并发环境中安全地存储和检索键值对。sync.Map 是在 Go 1.9 版本中引入的。
sync.Map 的主要特点和原理如下:
注意:sync.Map 并不适用于所有场景,它的性能通常不如单纯的 map,特别是在只有单一 Goroutine 访问的情况下。因此,只有在需要在多个 Goroutine 之间共享数据时,或者需要高度并发性能的情况下,才应该考虑使用 sync.Map。
golang 的 sync.Map 支持并发读写,采取“空间换时间”的机制,冗余了两个数据结构,分别是:read 和 dirty
type Map struct {
mu Mutex //互斥锁
read atomic.Value //无锁化读,包含两个字段:m map[interface}l*entry数据和amended bool标识只读map是否缺失数据
dirty map[interface}*entry //存在锁的读写
misses int //无锁化读的缺失次数
}
type entry struct{
p unsafe.Pointer
}
kv 中的 value,统一采用 unsafe.Pointer 的形式存储,通过 entry.p 指针进行链接。
entry.p 指向分为三种情况:
1. 存活态:正常指向元素。即 key-entry 对 仍未删除。
2. 软删除态:指向 nil。read map 和 dirty map 底层的 map 结构仍存在 key-entry 对,但是逻辑少那个该 key-entry 对已经被删除,因此无法被用户查询到。
3. 硬删除态:指向固定的全局变量 expunged。dirty map 中已不存在该 key-entry 对。
无锁化读的 m 是 dirty 的子集,amended 标识为 true 代表缺失数据,此时再去读 dirty,并把 misses + 1,当 misses 到一定阈值之后,同步 dirty 到 read 的 m 中。
和原始map+RWLock的实现并发的方式相比,减少了加锁对性能的影响。它做了一些优化:
slice1 := make([]int, 0)
slice2 := []int{}
var slice3 []int
slice4 := new([]int)
if slice1 == nil {
fmt.Println("slice1 is nil")
}
if slice2 == nil {
fmt.Println("slice2 is nil")
}
if slice3 == nil {
fmt.Println("slice3 is nil")
}
if *slice4 == nil {
fmt.Println("*slice4 is nil")
}
输出
slice3 is nil
*slice4 is nil
解释:
总结:
Go 语言的内存模型是一种并发编程模型,它提供了一些规则和保证,以便多个 Goroutine 可以安全地访问和修改共享的内存。Go 的内存模型具有以下主要特点:
在 Go 语言的内存管理中,小对象会增加垃圾收集(GC)的压力,主要是因为以下几个原因:
为了减轻小对象对 GC 的压力,可以考虑以下几种策略:
我认为Channel本质上就是一个线程安全的队列,用于协程间通讯的。
内部通过锁确保队列操作的原子性。
Channel 是异步进行的, channel 存在3种状态:
func deadlock1() {
ch := make(chan int)
ch <- 3 //这里会发生一直阻塞的情况,执行不到下一句
}
func deadlock2() {
ch := make(chan int)
ch <- 3 //这里会发生一直阻塞的情况,执行不到下一句
num := <- ch
fmt.Println("num=", num)
}
func deadlock3() {
ch := make(chan int, 3)
ch <- 3
ch <- 4
ch <- 5
ch <- 6 //这里会发生一直阻塞的情况
}
func deadlock4() {
ch := make(chan int)
fmt.Println(<- ch)
}
func deadlock5() {
ch1 := make(chan int)
ch2 := make(chan int) //互相等对方造成死锁
go func() {
for {
select {
case num := <- ch1:
fmt.Println("num=", num)
ch2 <- 100
}
}
}()
for {
select {
case num := <- ch2:
fmt.Println("num=", num)
ch1 <- 300
}
}
}
在 Go 语言中,原子操作用于确保多个 Goroutines 安全地对共享变量进行读写操作,以避免数据竞争和并发问题。Go 语言提供了一些原子操作函数,其中最常用的是 sync/atomic 包中的函数。
使用场景:
mutex
,还可以使用 sync/atomic
包的原子操作,它能够保证对变量的读取或修改期间不被其他的协程所影响。atomic
包提供的原子操作能够确保任一时刻只有一个 goroutine 对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。常见操作:
atomic 操作的对象是一个地址,你需要把可寻址的变量的地址作为参数传递给方法,而不是把变量的值传递给方法。下面分别介绍这些操作:
增减操作:此类操作的前缀为 Add。原子地将指定的整数值与另一个整数值相加。
func AddInt32(addr *int32,delta int32)(new int32)
func AddInt64(addr *int64, delta int64)(new int64)
func AddUint32(addr *uint32, delta uint32)(new uint32)
func AddUint64(addr *uint64,delta uint64)(new uint64)
func AddUintptr(addr *uintptr,delta uintptr)(new uintptr)
func add(addr *int64, delta int64) {
atomic.AddInt64(addr, delta)//加操作
fmt.Println("add opts: ",*addr)
}
载入操作:此类操作的前缀为 Load。原子地加载指定的 32 位或 64 位整数值。
func LoadInt32(addr *int32)(val int32)
func LoadInt64(addr *int64)(val int64)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func LoadUint32(addr *uint32)(val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
//特殊类型:Value类型,常用于配置变更
func (v *Value) Load() (x interface{}){}
比较并交换:此类操作的前缀为 CompareAndSwap,该操作简称 CAS,可以用来实现乐观锁。原子地比较指定的整数值和期望值,并在它们相等时进行交换。
func CompareAndSwapInt32(addr *int32, old, new int32)(swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32)(swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
该操作在进行交换前首先确保变量的值未被更改,即仍然保持参数 old 所记录的值,满足此前提下才进行交换操作。
CAS的做法类似操作数据库时常见的乐观锁机制。
注意,当有大量的goroutine对变量进行读写操作时,可能导致CAS操作无法成功,这时可以利用for循环多次尝试。
其他
atomic.StoreInt32 / atomic.StoreInt64: 用于原子地存储指定的 32 位或 64 位整数值。
atomic.SwapInt32 / atomic.SwapInt64: 用于原子地交换指定的整数值。
atomic.LoadPointer / atomic.StorePointer: 用于原子地加载和存储指针。
atomic.CompareAndSwapPointer: 用于原子地比较指定的指针和期望值,并在它们相等时进行交换。
atomic.AddUint32 / atomic.AddUint64: 用于原子地将指定的无符号整数值与另一个无符号整数值相加。
atomic.AddInt / atomic.AddUint: 用于原子地将指定的整数值与另一个整数值相加,其中整数的大小是平台相关的。
乐观锁适用于读多写少的情况,可以提高并发性,但需要处理冲突;
悲观锁适用于写多读少或需要强制资源独占的情况,但可能导致性能瓶颈。
Goroutine可以理解为一种Go语言的协程(轻量级线程),是Go支持高并发的基础,属于用户态的线程,由Goruntime管理而不是操作系统。
底层数据结构:
type g struct {
goid int64 //唯一的goroutine的ID
sched gobuf //goroutine切换时,用于保存g的上下文
stack stack //栈
gopc //pc of go statement that created this goroutine
startpc uintptr //pc of goroutine function
// ...
}
type gobuf struct {
sp uintptr //栈指针位置
pc uintptr //运行到的程序位置
g guintptr //指向goroutine
ret uintptr //保存系统调用的返回值
// ...
}
type stack struct {
lo uintptr //栈的下界内存地址
hi uintptr //栈的上界内存地址
}
goroutine的状态流转:
创建:go 关键字会调用底层函数 runtime.newproc()
创建一个 goroutine
,调用该函数之后,goroutine会被设置成 runnable
状态
运行:goroutine 本身只是一个数据结构,真正让 goroutine 运行起来的是调度器。Go实现了一个用户态的调度器(GMP模型),这个调度器充分利用现代计算机的多核特性,同时让多个 goroutine 运行,同时 goroutine 设计的很轻量级,调度和上下文切换的代价都比较小。
调度时机:
每个M开始执行P的本地队列中的G时,goroutine会被设置成 running 状态。
如果某个M把本地队列中的G都执行完成之后,然后就会去全局队列中拿G,这里需要注意,每次去全局队列拿G的时候,都需要上锁,避免同样的任务被多次拿。从全局队列取的G数量: N= min(len(GRQ)/GOMAXPROCS 1,len(GRQ/2)) (根据GOMAXPROCS负载均衡)
如果全局队列都被拿完了,而当前M也没有更多的G可以执行的时候,它就会去其他Р的本地队列中拿任务,这个机制被称之为work stealing机制,每次会拿走一半的任务,向下取整,比如另一个P中有3个任务,那一半就是一个任务。从其它P本地队列窃取的G数量: N=len(LRQ)/2 (平分)
当全局队列为空,M也没办法从其他的Р中拿任务的时候,就会让自身进入自旋状态,等待有新的G进来。最多只会有GOMAXPROCS个M在自旋状态,过多M的自旋会浪费CPU 资源。
阻塞:channel的读写操作、等待锁、等待网络数据、系统调用等都有可能发生阻塞,会调用底层函数runtime. gopark(),会让出CPU时间片,让调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。当调用该函数之后,goroutine会被设置成 waiting
状态。
唤醒:处于waiting状态的goroutine,在调用runtime.goready()函数之后会被唤醒,唤醒的goroutine会被重新放到M对应的上下文P对应的runqueue中,等待被调度。当调用该函数之后,goroutine会被设置成 runnable 状态。
退出:当goroutine执行完成后,会调用底层函数 runtime.Goexit() ,当调用该函数之后,goroutine会被设置成 dead
状态。
泄露原因:
在开发过程中,如果不对 goroutine 加以控制而进行滥用的化,可能会导致服务整体崩溃。比如耗尽系统资源导致程序崩溃,或者 CPU 使用率过高导致系统忙不过来。
在 golang 中,GOMAXPROCS 中控制的是未被阻塞的所有 goroutine,可以被 Multiplex 到多少个线程上运行,通过 GOMAXPROCS 可以查看 goroutine 的数量。
在 Go 中,可以使用 runtime 包来查看当前正在运行的 goroutine 的数量和限制 goroutine 的数量。以下是一些相关函数和方法:
查看当前正在运行的 goroutine 的数量:使用 runtime.NumGoroutine() 函数可以查看当前正在运行的 goroutine 的数量。该函数返回一个整数,表示当前活动的 goroutine 数量。例如:
numGoroutines := runtime.NumGoroutine()
fmt.Printf("当前活动的 goroutine 数量: %d\n", numGoroutines)
限制 goroutine 的数量: Go 语言本身没有提供内置的机制来限制 goroutine 的数量。但是,可以使用通道和协程来自行实现 goroutine 的数量限制。以下是一个示例:
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
// 模拟工作
fmt.Printf("Worker %d 开始处理 Job %d\n", id, job)
// 实际工作处理
results <- job * 2
fmt.Printf("Worker %d 完成处理 Job %d\n", id, job)
}
}
func main() {
numJobs := 10
numWorkers := 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// 启动 goroutine
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
worker(workerID, jobs, results)
}(i)
}
// 提交任务
for i := 1; i <= numJobs; i++ {
jobs <- i
}
close(jobs)
// 收集结果
go func() {
wg.Wait()
close(results)
}()
// 处理结果
for result := range results {
fmt.Printf("收到结果: %d\n", result)
}
}
通过使用两个通过 jobs 和 results 以及一个等待组 wg 来实现的。
jobs 通道:这个通道用于传递需要处理的任务(jobs)给 goroutine。在示例中,我们创建了10个任务(1 到 10),并将它们发送到 jobs 通道中。
results 通道:这个通道用于传递处理任务后的结果。每个 goroutine 将任务处理后的结果发送到 results 通道中。
wg 等待组:sync.WaitGroup 用于等待所有 goroutine 完成工作。我们在 main 函数中创建了一个 WaitGroup 变量 wg。每个启动的 goroutine 都会在完成工作后调用 wg.Done() 来通知 wg 已完成。最后,我们在一个单独的协程中调用 wg.Wait() 来等待所有 goroutine 完成。这确保了在程序退出之前等待所有 goroutine 完成。
这种设计模式允许我们限制并发执行的 goroutine 数量。在示例中,我们启动了3个工作 goroutine,它们从 jobs 通道中接收任务,处理任务后将结果发送到 results 通道。只有当一个 goroutine完成任务后,它才能从 jobs 通道中接收下一个任务。这样,限制了同时活动的 goroutine 数量。
在 Go 中,结构体(struct)可以进行比较操作,但有一些限制。结构体比较是按字段逐个比较的,但有一些规则和限制:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
p1 := Person{"Alice", 30}
p2 := Person{"Bob", 25}
p3 := Person{"Alice", 30}
// 结构体比较
fmt.Println("p1 == p2:", p1 == p2) // false
fmt.Println("p1 == p3:", p1 == p3) // true
}
在 Go 1.18 之前:
在 Go 1.18 之后:
Go 1.18 引入了更智能的扩容策略,以减少内存分配和复制的次数。具体策略如下:
这个更新的策略可以更好地平衡内存和性能。所以,扩容策略在 Go 1.18 之后有了一些改变,不再是固定的 2 倍或 1.25 倍,而是根据容量的大小来调整。
内存泄漏在 Go 中通常发生在以下情况下:
对于内存泄露的检测,Go 提供了一些工具来帮助检测盒分析内存泄露:
pprof
:Go 的标准库提供了 net/http/pprof 包,可以用于生成内存和 CPU 分析报告,以帮助诊断和定位内存泄漏问题。Gops
:Gops 是一个用于检查和操作正在运行的 Go 进程的工具,它提供了一些命令,如查看内存分配情况和垃圾回收的状态,以帮助识别内存泄漏。这些工具可以帮助你发现内存泄漏,并分析哪些部分的代码导致了泄漏,以便及时修复问题。
可能不相等。在Go中,接口值包括两个部分:类型(Type)和值(Value)。当接口值的类型部分为 nil 且值部分也未设置时,接口值等于 nil。这是因为接口类型的零值就是 nil。
两个接口值比较时,会先比较 T,再比较 V。
接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
func main() {
var p *int
var i interface{} = p
fmt.Println(i == p) // true 接口 i 的值部分包含了指针 p,而接口值的比较是按照 i 的类型和值进行的,所以 i 等于 p。
fmt.Println(p == nil) // true 指针 p 确实是 nil
fmt.Println(i == nil) // false 虽然 i 的值部分包含 nil 指针,但由于 i 的类型部分不是 nil,因此整个接口值 i 不等于 nil。这是因为接口值的比较是按照类型和值一起进行的。
}
CPU 并不会以一个一个字节去读取和写入内存。相反 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度,内存访问粒度跟机器字长有关。
对齐规则:
对齐规则确保了结构体和其成员变量按照合适的方式在内存中排列,以便于 CPU 高效地访问数据。对于不同的编译器和目标平台,对齐规则可能会有所不同。
reflect.TypeOf(a).Kind() == reflect.TypeOf(b).Kind()
reflect.DeepEqual(a, b)
reflect.ValueOf(&a).Elem().Set(reflect.ValueOf(b))
%v:只输出所有的值
%+v:先输出字段名字,再输出该字段的值
%#v:先输出结构体名字值,再输出结构体(字段名字+字段的值)
package main
import "fmt"
type student struct {
id int32
name string
}
func main() {
a := &student{id: 1, name: "微客鸟窝"}
fmt.Printf("a=%v \n", a) // a=&{1 微客鸟窝}
fmt.Printf("a=%+v \n", a) // a=&{id:1 name:微客鸟窝}
fmt.Printf("a=%#v \n", a) // a=&main.student{id:1, name:"微客鸟窝"}
}
在 Go 中,字符有两种类型:
空结构体 struct{} 实例不占据任何的内存空间。
用途:
golang 函数与方法的区别是:方法有一个接收者。
如果方法的接收者是指针类型,无论调用者是对象还是对象指针,修改的都是对象本身,会影响调用者。
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者。
package main
import "fmt"
type Person struct {
age int
}
func (p *Person) IncrAge1() {
p.age += 1
}
func (p Person) IncrAge2() {
p.age += 1
}
func (p Person) GetAge() int {
return p.age
}
func main() {
p := Person{
22,
}
p.IncrAge1()
fmt.Println(p.GetAge()) //23
p.IncrAge2()
fmt.Println(p.GetAge()) //23
p2 := &Person{
age: 22,
}
p2.IncrAge1()
fmt.Println(p2.GetAge()) //23
p2.IncrAge2()
fmt.Println(p2.GetAge()) //23
}
通常使用指针类型作为方法的接收者的理由:
在 Go 中,每个函数(或方法)都有一个数据结构,其中包含一堆 defer 函数的指针。当你使用 defer
关键字来推迟函数的执行时,Go 编译器会将要推迟的函数包装成一个闭包(closure)并存储到函数的 defer 链表中。
这个 defer 链表就像一个栈,后添加的 defer 函数会放在链表的前面,这意味着最后添加的 defer 函数会最先执行。当函数正常返回时,Go 会按照 后进先出(LIFO) 的顺序执行这些 defer 函数,以确保资源的释放和清理。
如果在函数中遇到了 panic,Go 会立即停止正常执行路径,但会保留 defer 链表。之后,defer 链表中的函数将按照后进先出的顺序执行,这可以用来处理 panic、恢复程序状态或执行资源清理等操作。
所以,defer 的实现基本上是通过将要推迟执行的函数封装成闭包,并按照后进先出的顺序执行他们,以确保在函数返回或 panic 时执行特定的操作。
package main
import "fmt"
func F() {
defer func() {
fmt.Println("b")
}()
panic("a")
}
func main() {
defer func() {
fmt.Println("c")
}()
//子函数抛出的panic没有recover时,上层函数时,程序直接异常终止
F()
fmt.Println("继续执行")
}
package main
import "fmt"
func F() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常", err)
}
fmt.Println("b")
}()
panic("a")
}
func main() {
defer func() {
fmt.Println("c")
}()
//子函数抛出的panic没有recover时,上层函数时,程序直接异常终止
F()
fmt.Println("继续执行")
}
select 是 Go 语言中用于处理并发操作的一种控制结构。它允许你在多个通道操作之间进行选择,以便在其中一个操作准备好数据时执行相应的操作。select的底层原理:
select的底层原理是通过检查多个通道操作,选择一个可以立即执行的操作,或等待至少一个操作准备好。
gRPC是基于go的远程过程调用。RPC 框架的目标就是让远程服务调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。
反射是指计算机程序可以访问、检测和修改它本身状态或行为的一种能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,比如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期间获得类型的反射信息,并且有能力修改它们。
反射的两个弊端:
golang 当中反射最重要的两个概念是 Type 和 Value。
对于字符串拼接,推荐使用 strings.Builder
或 bytes.Buffer
,它们的性能更高,因为它们是基于 []byte
实现的缓冲区,可以避免不必要的内存分配和拷贝。同时,这两个类型也提供了更多的字符串操作方法。
使用 +
拼接字符串:因为golang的字符串是静态的,所以每次+
都会重新分配一个内存空间存相加的两个字符串。
+
操作符拼接字符串时,会创建一个新的字符串,并将原始字符串的内容和要拼接的字符串复制到新的内存位置。+
操作符相当于使用 append
函数将字节片段追加到切片中,然后将该切片转换为字符串。使用 fmt.Sprintf
拼接字符串:主要是使用到了反射。
fmt.Sprintf
是通过格式化字符串来拼接各种数据类型,并返回一个新的字符串。fmt.Sprintf
在内部使用反射来动态的将各种数据类型转化为字符串,然后将他们拼接在一起。fmt.Sprintf
可以处理各种不同的数据类型,但是这也会引入一些性能开销,因为反射在运行时会检查和转换数据类型。使用 strings.Builder
:(golang 1.10及以后版本可用)
strings.Builder 是一个字符串缓冲区,用于构建字符串。
它提供了方法来追加字符串,比如 WriteString
、Write
等,以及 String
方法用于获取最终的字符串。
使用 strings.Builder 时,可以连续追加字符串,而不会导致大量的内存分配。它内部维护一个 []byte
,动态调整大小,以容纳追加的内容。
示例:
var builder strings.Builder
builder.WriteString("hello, ")
builder.WriteString("world!")
result := builder.String() //获取最终字符串
addr 字段主要是做 copycheck,buf 字段是一个 byte 类型的切片,这个就是用来存放字符串内容的,提供的 writeString() 方法就是像切片buf中追加数据。
[]byte的申请是成倍的,例如,初始大小为 0,当第一次写入大小为 10 byte 的字符串时,则会申请大小为 16 byte 的内存(恰好大于 10 byte 的 2 的指数),第二次写入 10 byte 时,内存不够,则申请 32 byte 的内存,第三次写入内存足够,则不申请新的,以此类推。
提供的 String 方法就是将 []byte 转换为 string 类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝。
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte // 1
}
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
使用 bytes.Buffer
:
WritrString
、Write
等。var buffer bytes.Buffer
buffer.WriteString("hello, ")
buffer.WriteString("world!")
result := buffer.String() //获取最终字符串
type stringStruct struct {
str unsafe.Pointer
len int
}
str := "hello"
// string 转为 []byte
bytes := []byte(str)
// 修改 []byte
bytes[0] = 'H' // 可以修改成功
// []byte 转为 string
str = string(bytes)
在这个过程中,由于字符串的不可变性,实际上是创建了一个新的字符串。HTTP:是一种应用层协议,通常用于在客户端和服务器之间传输超文本文档(如网页)的协议。基于文本,可读性强的协议。
RPC:是一种远程过程调用协议,用于实现不同计算机或进程之间的函数调用和数据交换。通常以编程语言特定的方式定义服务和数据类型。
相同点:
不同点:
RPC:是一种远程过程调用机制,允许客户端程序调用远程服务器上的函数或方法,就像本地函数调用一样。
gRPC:是一个高性能、跨语言的远程过程调用框架,使用 Protocol Buffers 作为接口描述语言和二进制数据序列化格式。
相同点:
不同点:
sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担;
sync.Pool 中保存的元素有如下特征:
sync.Pool 的数据结构:
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() any
}
申请对象 Get
释放对象 Put
初始化 Pool 示例
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var createNum int32
func createBuffer() interface{} {
atomic.AddInt32(&createNum, 1)
buffer := make([]byte, 1024)
return buffer
}
func main() {
bufferPool := &sync.Pool{New: createBuffer}
workerPool := 1024 * 1024
var wg sync.WaitGroup
wg.Add(workerPool)
for i := 0; i < workerPool; i++ {
go func() {
defer wg.Done()
buffer := bufferPool.Get()
_ = buffer.([]byte)
// buffer := createBuffer()
// _ = buffer.([]byte)
defer bufferPool.Put(buffer)
}()
}
wg.Wait()
fmt.Printf(" %d buffer objects were create.\n", createNum)
time.Sleep(3 * time.Second)
}
jwt 结构:JWT 由三部分组成,它们以点号分隔:
jwt 认证授权过程: