原文链接
make(chan int, 1)
和 make(chan int)
之间有区别。
make(chan int, 1)
创建了一个有缓冲的通道,容量为1。这意味着通道可以缓存一个整数元素,即使没有接收方,发送操作也不会被阻塞,直到通道已满。如果没有接收方,发送操作会立即完成。如果通道已满,发送操作会被阻塞,直到有接收方接收数据。这种通道适用于发送方和接收方的速度不一致的情况。make(chan int)
创建了一个无缓冲的通道,容量为0。这意味着通道没有缓冲区,发送操作会阻塞直到有接收方接收数据,而接收操作也会阻塞直到有发送方发送数据。这种通道适用于同步操作,即发送方和接收方需要同步地进行数据交换。因此,根据具体的需求和使用场景,选择适当的通道类型是很重要的。如果需要在发送和接收之间有一定的缓冲空间,可以使用有缓冲通道;如果需要同步操作,确保发送和接收的同步性,可以使用无缓冲通道。
Go中的channel是一种用于在不同goroutine之间进行通信和同步的基本构造。它可以用于将数据从一个goroutine发送到另一个goroutine,也可以用于实现多个goroutine之间的同步。channel是一种线程安全的数据结构,可以保证多个goroutine之间的数据传输是安全的。
关于channel的内部实现原理,可以简要说明以下几点:
close()
函数关闭channel,关闭后的channel无法再发送数据,但可以继续接收数据。关闭后的channel如果没有接收完所有数据,接收操作仍然可以继续,但会接收到零值。目前的go GC采用三色标记法和混合写屏障技术。
Go GC有四个阶段:
Go语言的垃圾回收(Garbage Collection,简称GC)是自动内存管理的一种机制,用于自动检测和回收不再使用的内存,减轻了程序员手动管理内存的负担,提高了程序的健壮性和开发效率。
Go的GC过程可以简要概括为以下几个步骤:
Go的GC采用了三色标记法(Tri-color Marking),即将对象分为三种颜色:白色、灰色和黑色。
GC会从根对象开始,将所有根对象标记为灰色,并将其放入待处理队列。然后从待处理队列中取出一个灰色对象,将其标记为黑色,并将其子对象标记为灰色,再放入待处理队列。重复该过程,直到待处理队列为空,所有可达对象都被标记为黑色。
值得注意的是,Go的GC使用了并发标记和并发清除的策略,即GC过程与程序的执行并发进行,减少了GC对程序执行的影响。同时,Go的GC还实现了增量标记和并发回收,将GC的时间分摊到多个小步骤,降低了GC造成的延迟。
在 Go 的垃圾收集过程中,是有可能发生一个对象在前一时刻被标记为垃圾(即不再被引用)而在下一时刻又不是垃圾(重新被引用)的情况。
这种情况被称为 “retracing”,也就是在垃圾收集器扫描对象图的过程中,有新的引用或修改了旧引用,导致之前被标记为垃圾的对象变为可达,从而避免了被回收。
Go 的垃圾收集器使用的是 “标记-清除”(Mark and Sweep)算法,其中 “标记” 阶段会遍历对象图,并标记所有可达的对象,而 “清除” 阶段则回收没有被标记的对象。
在并发的情况下,当垃圾收集器进行标记阶段时,程序可能在其他 goroutines 中产生新的对象引用或修改旧引用。这些引用的变化可能会导致之前被标记为垃圾的对象重新变为可达状态,从而不会被回收。
Go 的垃圾收集器使用了写屏障(Write Barrier)等技术来处理并发情况下的对象引用变化,以确保垃圾收集过程的正确性。但是,由于并发情况下的引用变化是动态的,垃圾收集器可能需要重新追踪对象,这就是 “retracing” 的原因。
虽然 “retracing” 存在,但 Go 的垃圾收集器已经经过精心设计和优化,以在大多数情况下提供高效的垃圾回收。对于大多数应用来说,不必过于担心 “retracing” 的问题,垃圾收集器会自动根据程序的运行情况动态地调整自己的行为,以达到尽可能高效的回收效果。
写屏障(Write Barrier)是一种在并发垃圾收集过程中用于处理对象引用变化的技术。在并发情况下,当一个 goroutine 修改了一个对象的引用关系时,其他的 goroutines 可能同时访问这个对象,而这时垃圾收集器正在对对象图进行标记。
写屏障技术的目的是确保垃圾收集器能够正确地跟踪引用关系,避免漏掉已经变为不可达的对象或错误地回收仍然可达的对象。
具体来说,写屏障技术在发生写操作时,会插入一些特殊的代码来通知垃圾收集器有一个引用关系的变化。这样,垃圾收集器就能够正确地追踪到新的引用关系,将新的引用标记为可达,或者取消旧的引用的标记。这样,在并发垃圾收集过程中,即使有其他 goroutines 在修改引用关系,垃圾收集器仍然能够正确地进行标记和回收。
Go 语言的垃圾收集器就使用了写屏障技术来处理并发情况下的对象引用变化。通过使用写屏障,Go 的垃圾收集器能够保证垃圾回收过程的正确性,并在大多数情况下提供高效的垃圾回收性能。
需要注意的是,写屏障技术可能会在一定程度上带来一些额外的开销,因为它需要在写操作时插入额外的代码。但是,这个开销通常是值得的,因为它保证了垃圾收集器的正确性和性能,并帮助 Go 语言实现高效的并发垃圾回收。
拼接字符串的方式有:+
, fmt.Sprintf
, strings.Builder
, bytes.Buffer
, strings.Join
使用+
操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。
由于采用了接口参数,必须要用反射获取值,因此有性能损耗。
用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。
bytes.Buffer
是一个一个缓冲byte
类型的缓冲器,这个缓冲器里存放着都是byte
,
bytes.buffer
底层也是一个[]byte
切片。
strings.join
也是基于strings.builder
来实现的,并且可以自定义分隔符,在join方法内调用了b.Grow(n)方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。
性能比较:
strings.Join ≈ strings.Builder > bytes.Buffer > “+” > fmt.Sprintf
在Go语言的面试中,关于切片的问题是很常见的。以下是一些可能出现的面试题:
在 Go 中,数组(Array)和切片(Slice)是两种不同的数据类型,它们具有一些区别。
[5]int
表示长度为 5 的整型数组。而切片可以使用 []Type
的方式声明,例如 []int
表示一个整型切片。在实际应用中,切片更加常用,因为它提供了更多的灵活性和便利性。切片可以根据需求动态调整大小,并且在函数传递和返回时更加高效和方便。数组在特定场景下仍然有其用处,例如固定大小的数据集合或对内存布局有特殊要求的情况下。
在Go语言中,切片是不可比较的。切片是引用类型,包含指向底层数组的指针、切片的长度和容量。由于切片是动态长度的,即可以动态增长或缩减,因此在切片的比较过程中可能涉及到多个底层数组的元素,这使得切片的比较变得复杂。
在Go语言中,切片的比较是不允许的,因为切片的底层数据可能在内存中的不同位置,即使它们的元素相同,它们的指针地址也不相同。这就导致了在比较两个切片时,无法简单地通过比较指针地址或元素值来确定它们是否相等。
如果需要判断两个切片是否包含相同的元素,可以通过遍历切片的元素逐个比较来实现,但不能直接通过 ==
操作符来比较整个切片。例如,可以使用循环或者使用reflect包来编写一个函数来判断两个切片是否相等,但是不能直接使用 s1 == s2
来判断两个切片是否相等。
总结:在Go语言中,切片是不可比较的,不能直接使用 ==
操作符来比较两个切片是否相等,需要使用其他方法来进行切片的比较。
在 Go 中,new
和 make
是两个用于创建数据结构的内建函数,它们有不同的使用场景和作用。
new
函数:new
用于创建某种类型的指针,并返回该指针的零值。它接受一个参数,即要创建的类型,返回一个指向该类型的零值的指针。使用 new
函数适用于创建值类型(如结构体、整型、浮点型等)的指针。
示例:ptr := new(int)
,在这个示例中,new(int)
创建了一个 int
类型的指针,并将其初始化为 int
类型的零值。
make
函数:make
用于创建切片、映射和通道等引用类型的数据结构,并进行初始化。它接受两个或三个参数,第一个参数是要创建的类型,后面的参数根据类型的不同而有所不同。
slice
):make([]T, length, capacity)
,其中 T
是切片的元素类型,length
是切片的长度,capacity
是切片的容量。map
):make(map[K]V, capacity)
,其中 K
是键的类型,V
是值的类型,capacity
是映射的初始容量。channel
):make(chan T, capacity)
,其中 T
是通道中元素的类型,capacity
是通道的缓冲区容量。make
函数适用于创建引用类型的数据结构,并且会进行初始化,返回一个已经初始化的引用类型的值。
总结:new
用于创建值类型的指针,并返回指针的零值;make
用于创建引用类型的数据结构,并进行初始化。根据不同的需求和数据类型,选择适当的函数来创建相应的数据结构。
在 Go 中,使用 new
初始化 map
、slice
和 channel
会得到它们的零值,并返回一个指向该类型的指针。
初始化 map
: 使用 new
初始化 map
会得到一个指向 map
类型的指针,其值为 nil
,表示一个空的 map
。需要注意的是,尽管 map
被初始化为 nil
,但是仍然可以进行操作,例如对其进行赋值、添加键值对等操作。然而,在使用 map
前,需要确保对其进行初始化,通常使用 make
来创建非空的 map
。
示例:
goCopy codem := new(map[string]int)
fmt.Println(m) // 输出: &map[]
初始化 slice
: 使用 new
初始化 slice
会得到一个指向 slice
类型的指针,其值为 nil
,表示一个空的 slice
。与 map
类似,尽管 slice
被初始化为 nil
,但是仍然可以进行操作,例如对其进行赋值、追加元素等操作。在使用 slice
前,同样需要对其进行初始化,通常使用 make
来创建非空的 slice
。
示例:
goCopy codes := new([]int)
fmt.Println(s) // 输出: &[]
初始化 channel
: 使用 new
初始化 channel
会得到一个指向 channel
类型的指针,其值为 nil
,表示一个未初始化的 channel
。这样的 channel
无法直接使用,需要使用 make
创建一个具体的通道并分配相应的缓冲区大小后才能使用。
示例:
goCopy codech := new(chan int)
fmt.Println(ch) // 输出:
综上所述,虽然可以使用 new
初始化 map
、slice
和 channel
,但得到的是一个指向对应类型的指针,其值为 nil
。如果需要创建一个非空的 map
、slice
或 channel
,通常建议使用 make
函数进行初始化,并分配相应的内存和缓冲区。
可能不等。interface在运行时绑定值,只有值为nil接口值才为nil,但是与指针的nil不相等。举个例子:
var p *int = nil
var i interface{} = nil
if(p == i){
fmt.Println("Equal")
}
两者并不相同。总结:两个nil只有在类型相同时才相等。
gRPC(gRPC Remote Procedure Call)是一种高性能、通用的开源远程过程调用(RPC)框架,由Google开发并开源。它基于HTTP/2协议,并使用Protocol Buffers作为默认的序列化机制。
gRPC旨在简化分布式应用程序之间的通信,使得客户端和服务器可以像调用本地方法一样调用远程服务。它支持多种编程语言(如Go、Java、C++、Python等),允许开发者使用定义服务接口的方式来描述请求和响应的数据结构,然后自动生成相应的客户端和服务器端代码。
gRPC具有以下特点和优势:
总之,gRPC是一种功能强大的远程过程调用框架,提供了高性能、跨平台和多语言支持,适用于构建分布式系统中的服务间通信。它简化了开发者的工作,提供了简洁、高效和可扩展的解决方案。
当进程被kill时,操作系统会终止进程并清理相关资源,包括所有的goroutine。在这种情况下,无法直接保证所有的goroutine能够顺利退出。不过,可以采取一些措施来优雅地退出goroutine并确保资源的正确释放。
os/signal
包来捕获信号并执行相应的处理逻辑。context
来控制goroutine:在启动goroutine时,传递一个context.Context
对象给它,可以通过该对象来控制goroutine的生命周期。在进程即将退出时,可以调用cancel
函数取消所有相关的context
,从而通知goroutine退出。sync.WaitGroup
等待goroutine退出:在主goroutine中,使用sync.WaitGroup
来等待所有的goroutine完成。在goroutine退出时,通过Done
方法减少WaitGroup
的计数器,主goroutine可以通过Wait
方法等待所有的goroutine退出。需要注意的是,对于一些无法中断或阻塞的操作,比如网络请求或文件IO等,可能需要通过设置超时或取消的机制来确保它们能够及时退出。
综上所述,保证所有goroutine顺利退出的方法可以结合使用信号通知、使用context
控制、使用sync.WaitGroup
等待、使用退出通道等多种手段。具体的实现方式取决于应用的需求和场景。
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
// 启动多个goroutine
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Goroutine %d: Exiting\n", id)
return
default:
fmt.Printf("Goroutine %d: Running\n", id)
time.Sleep(time.Second)
}
}
}(i)
}
// 捕获终止信号
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
// 等待终止信号
<-signalCh
fmt.Println("Received termination signal. Cleaning up...")
// 发送取消信号,通知所有goroutine退出
cancel()
// 等待所有goroutine退出
wg.Wait()
fmt.Println("All goroutines have exited. Exiting main...")
}
context
控制:goCopy codepackage main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
// 启动多个goroutine
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Goroutine %d: Exiting\n", id)
return
default:
fmt.Printf("Goroutine %d: Running\n", id)
time.Sleep(time.Second)
}
}
}(i)
}
// 等待一段时间后,取消所有goroutine
time.Sleep(5 * time.Second)
cancel()
// 等待所有goroutine退出
wg.Wait()
fmt.Println("All goroutines have exited. Exiting main...")
}
在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。
暂时性内存泄露
string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏
永久性内存泄露
在Go中,goroutine会在其执行完成后自动被垃圾回收,因此不会出现传统意义上的内存泄漏。然而,有一些情况可能导致goroutine无法正常退出,从而导致资源泄漏或无法释放的问题。以下是一些可能导致goroutine泄漏的情况以及如何避免它们:
- 未关闭的通道(channel):如果一个goroutine向一个通道发送数据,而没有其他goroutine接收该数据,该goroutine将会被阻塞并无法退出。为了避免这种情况,确保在不需要继续发送数据时,正确地关闭通道。
- 循环引用:如果一个goroutine持有对其他对象的引用,而这些对象又持有对该goroutine的引用,就会形成循环引用。这可能导致垃圾回收器无法回收这些对象,从而导致内存泄漏。为了避免循环引用,确保在不再需要时,及时解除对对象的引用。
- 资源未释放:如果goroutine在完成任务后没有正确释放所使用的资源(如打开的文件、数据库连接等),就可能导致资源泄漏。确保在不再需要资源时,及时进行关闭、释放或销毁。
- 无限循环或阻塞:如果goroutine进入无限循环或阻塞状态,它将无法正常退出并释放相关资源。确保goroutine的执行逻辑能够合理终止或定时退出,避免陷入无限循环或永久阻塞的情况。
- 忘记等待goroutine完成:如果主goroutine在退出前没有等待其他goroutine完成,可能会导致这些goroutine无法完成任务或资源清理。使用
sync.WaitGroup
等机制来等待所有需要等待的goroutine完成。总之,避免goroutine的内存泄漏主要需要确保通道的正确关闭、避免循环引用、及时释放资源、避免无限循环或阻塞,以及适当等待其他goroutine完成。通过合理的设计和资源管理,可以避免大多数goroutine泄漏和资源泄漏问题。
defer
语句是Go语言中的一种特性。它用于在函数执行完毕后或发生panic
时,按照后进先出(LIFO)的顺序执行一系列预定的函数调用。defer
语句非常实用,可以确保某些清理或收尾工作无论函数是如何退出(正常返回、panic
或运行时错误)都会被执行。
在Go的运行时中,defer
语句通过类似堆栈的数据结构实现。当遇到defer
语句时,函数调用和其参数会被推入一个defer
栈的顶部。栈会保持defer
语句执行的顺序,因此最后被推入栈的defer
语句会最先执行,而最先被推入栈的会最后执行,等到包围的函数返回时进行逆序执行。
当包围的函数执行完毕,无论是正常返回还是panic
,Go运行时会开始依次执行defer
栈中的函数调用。即使发生了panic
,defer
语句也会被执行,这样在panic
向上传播之前可以确保关键的清理或资源释放操作。
下面是一个示例,演示了defer
语句的行为:
package main
import "fmt"
func cleanup() {
fmt.Println("执行清理操作...")
}
func main() {
fmt.Println("开始主函数")
defer fmt.Println("延迟执行语句 1")
defer fmt.Println("延迟执行语句 2")
defer cleanup()
panic("发生了 panic")
fmt.Println("结束主函数") // 这行代码永远不会被执行
}
开始主函数
执行清理操作…
延迟执行语句 2
延迟执行语句 1panic: 发生了 panic
goroutine 1 [running]:
main.main()
/路径/文件.go:13 +0x9a
exit status 2
在go里定义错误异常的方式有这么两种,但都需要你的返回值是error类型的:
package main
import (
"errors"
"fmt"
)
type equalError struct {
Num int
}
//方法名字是Error()
func (e equalError) Error() string {
return fmt.Sprintf("当前数字是 %d ,大于10", e.Num)
}
//使用errors.New简单生成
func Equal(n int) (int, error) {
if n > 10 {
return -1, errors.New("大于10") //生成一个简单的 error 类型
}
return n, nil
}
func DiyEqual(n int) (int, error) {
if n > 10 {
return -1, equalError{Num: n} // 会调用equalError的Error方法
}
return n, nil
}
func main() {
//使用errors.New生成error对象
if result, err := Equal(20); err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
//不适用erros,自定义错误方式.
if result, err := DiyEqual(20); err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
}
在Go语言的面试中,关于map的问题也是很常见的。以下是一些可能出现的面试题:
在Go中,map的底层实现是一个哈希表(hash table),也称为散列表。哈希表是一种用于存储键值对的数据结构,它通过将键映射到哈希值,然后将哈希值映射到数组的索引来快速访问和查找数据。
具体来说,Go中的map是由一个hmap
结构体表示的,它定义在runtime/map.go
中,部分结构如下:
goCopy codetype hmap struct {
count int // 当前存储的键值对数量
flags uint8 // 状态标志,例如是否是扩容中
B uint8 // 桶的大小的移位数,用于计算桶的数量,比如2^B
noverflow uint16 // 溢出桶的数量,用于解决哈希冲突
hash0 uint32 // 哈希种子,用于计算哈希值
buckets unsafe.Pointer // 指向桶数组的指针
oldbuckets unsafe.Pointer // 指向扩容前的桶数组的指针,用于迁移数据
}
type bmap struct {
topbits [8]uint8 // 桶的最高8位的哈希值,用于快速定位
keys [8]keytype // 键数组
values [8]valuetype // 值数组
overflow uintptr // 指向溢出桶的指针,用于解决哈希冲突
}
Go的map使用了哈希表的经典实现,具有以下特点:
总之,Go中的map是一个高效、动态扩容的哈希表实现,可以在常数时间内进行插入、查找和删除操作。但是需要注意在并发场景下的正确使用,避免因为并发访问而引发竞态条件。
在Go中,map的key可以是以下几种类型:
但是,有一些类型是不允许作为map的key的:
值得注意的是,使用结构体类型作为map的key时,只有当结构体的所有字段都是可比较的才可以。如果结构体中包含不可比较的字段(如切片或函数),那么该结构体也不能作为map的key。
Go和Java是两种不同的编程语言,它们在许多方面有着显著的区别。以下是Go和Java之间的一些主要区别:
finalize()
方法)。