✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
41. Go channel 有什么特点?
channel 有 2 种类型:无缓冲、有缓冲
channe l有 3 种模式:写操作模式(单向通道)、读操作模式(单向通道)、读写操作模式(双向通道)
写操作模式 | 读操作模式 | 读写操作模式 | |
创建 | make(chan<- int) | make(<-chan int) | make(chan int) |
channel 有 3 种状态:未初始化、正常、关闭
未初始化 | 关闭 | 正常 | |
关闭 | panic | panic | 正常关闭 |
发送 | 永远阻塞导致死锁 | panic | 阻塞或者成功发送 |
接收 | 永远阻塞导致死锁 | 缓冲区为空则为零值,否则可以继续读 | 阻塞或者成功接收 |
注意点:
-
一个 channel 不能多次关闭,会导致 painc。
-
如果多个 goroutine 都监听同一个 channel,那么 channel 上的数据都可能随机被某一个 goroutine 取走进行消费。
-
如果多个 goroutine 监听同一个 channel,如果这个 channel 被关闭,则所有 goroutine 都能收到退出信号。
42. Go 语言当中 Channel(通道)有什么特点,需要注意什么?
在 Go 语言中,Channel 是一种用于 Goroutine 之间通信和同步的重要机制。Channel 具有以下几个特点:
-
线程安全:Channel 可以安全地在多个 Goroutine 之间传递数据,避免了数据竞争和死锁等问题。
-
阻塞式:当 Channel 中没有数据时,读取操作会被阻塞,直到 Channel 中有数据可读;同样地,当 Channel 已满时,写入操作会被阻塞,直到 Channel 中有空间可写入。
-
有缓冲和无缓冲:Channel 可以带有缓冲或者不带缓冲。不带缓冲的 Channel 可以保证每次写入和读取都是同步的;带缓冲的 Channel 可以在缓冲区未满时进行写入操作而不阻塞,直到缓冲区满时再阻塞写入操作。
-
可关闭:Channel 可以被显式地关闭,以通知 Channel 的接收方不再有数据可读,避免接收方被永久地阻塞。
在使用 Channel 时,需要注意以下几个问题:
-
避免死锁:当使用 Channel 进行 Goroutine 之间的通信和同步时,需要确保不会出现死锁的情况。一般来说,可以使用 select 语句和超时机制等方式来避免 Channel 的阻塞问题。
-
避免竞态条件:当多个 Goroutine 访问同一个 Channel 时,需要注意避免竞态条件的发生。可以使用 Mutex 和 sync 包中提供的其他同步机制来避免并发访问 Channel 导致的问题。
-
合理使用缓冲:当使用带缓冲的 Channel 时,需要根据实际需要设置缓冲区的大小,避免缓冲区过大或过小导致的性能问题。同时需要注意,当 Channel 中的数据过多时,会导致内存占用过高,需要及时清理不必要的数据。
-
避免 Channel 泄漏:当使用 Channel 时,需要注意避免 Channel 泄漏的问题,即在不需要使用 Channel 时及时关闭 Channel,避免 Channel 占用过多的系统资源。
实例
以下是一个使用同步锁和 Channel 进行并发编程的例子:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
ch := make(chan int, 5)
mutex := sync.Mutex{}
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
mutex.Lock()
ch <- i
mutex.Unlock()
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
mutex.Lock()
fmt.Println(<-ch)
mutex.Unlock()
}
}()
wg.Wait()
close(ch)
}
上述代码中,我们创建了一个有缓冲的 Channel,使用一个 Goroutine 向其中写入数据,另一个 Goroutine 从中读取数据,并使用同步锁保证对 Channel 的访问是线程安全的。在主函数中,我们使用 sync.WaitGroup 来等待两个 Goroutine 完成任务,并在任务完成后关闭 Channel。
43. Go 语言当中 Channel 缓冲有什么特点?
在 Go 语言中,Channel 缓冲是指在创建 Channel 时设置的缓冲区大小。带缓冲的 Channel 可以在缓冲区未满时进行写入操作而不阻塞,直到缓冲区满时再阻塞写入操作。
Channel 缓冲的特点如下:
-
可以提高并发性能:使用带缓冲的 Channel 可以提高并发程序的性能,因为缓冲区可以暂时存储数据,避免了每次数据传输时都需要阻塞等待的情况。这种方式特别适用于生产者-消费者模式,其中生产者的产生速度快于消费者的处理速度,缓冲区可以暂时存储一定量的数据,使得生产者和消费者的速度可以适度地解耦。
-
缓冲区大小需要合理设置:Channel 缓冲区大小的设置需要根据实际应用场景进行合理的选择,过小的缓冲区可能会导致生产者被阻塞,过大的缓冲区可能会导致内存占用过高。一般来说,需要根据实际情况进行调整,以达到最优的性能表现。
-
带缓冲的 Channel 可能会出现死锁问题:当使用带缓冲的 Channel 进行 Goroutine 之间的通信和同步时,需要注意避免死锁的问题。因为带缓冲的 Channel 可以在缓冲区未满时进行写入操作,如果生产者写入数据的速度过快,可能会导致缓冲区已满而阻塞生产者,此时如果消费者已经不再消费数据,整个程序就会进入死锁状态。
-
可以使用 close() 函数关闭 Channel:当使用带缓冲的 Channel 时,需要注意及时清理缓冲区中的数据,可以使用 close() 函数来显式地关闭 Channel。关闭 Channel 会使得 Channel 中未被读取的数据被丢弃,并且后续的写入操作会导致 panic 异常。
实例
举个例子,假设有一个生产者-消费者模式的场景,生产者不断地向 Channel 中写入数据,而消费者则以固定的速度从 Channel 中读取数据进行处理。如果使用带缓冲的 Channel,可以设置缓冲区大小为一定的值,比如 10,这样生产者可以连续向 Channel 中写入 10 个数据,只有当 Channel 中已经存储了 10 个数据时才会阻塞。而消费者则可以按照自己的处理速度从 Channel 中读取数据,只有当 Channel 中的数据被消费完时才会阻塞等待新的数据。这样可以提高程序的并发性能,避免频繁地阻塞等待。
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 1; i <= 10; i++ {
ch <- i
fmt.Printf("Producer: %d\n", i)
}
close(ch)
}
func consumer(ch <-chan int) {
for {
data, ok := <-ch
if !ok {
break
}
fmt.Printf("Consumer: %d\n", data)
time.Sleep(time.Second)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch)
consumer(ch)
}
44. C hannel 的 ring buffer 实现
在 Go 语言中,channel 是一种用于在 goroutine 之间进行通信的机制。通常情况下,channel 会被实现为一个 FIFO 的队列。当向 channel 发送数据时,数据会被添加到队列的末尾;当从 channel 接收数据时,数据会被从队列的头部取出。
在 Go 1.3 版本中,新增了一种基于环形缓冲区(ring buffer)的 channel 实现方式,可以用于提高 channel 的性能。具体来说,当创建一个缓冲区大小为 n 的 channel 时,Go 语言会为其分配一个大小为 n 的环形缓冲区,而不是一个简单的队列。
使用环形缓冲区实现 channel 有以下几个好处:
-
避免动态内存分配:在缓冲区大小确定的情况下,环形缓冲区可以在创建时一次性分配所需的内存,避免了频繁的动态内存分配和释放操作,从而提高了性能。
-
提高缓存命中率:环形缓冲区会将元素放置在连续的内存块中,这样可以提高缓存命中率,从而减少缓存访问延迟,提高了通信的效率。
-
支持无锁访问:由于 channel 是在多个 goroutine 之间进行通信的,因此通常会涉及到并发访问的问题。环形缓冲区的实现可以采用无锁算法,从而避免了锁竞争带来的开销,提高了并发访问的效率。
需要注意的是,使用环形缓冲区实现 channel 也有一些限制和注意事项。例如,缓冲区大小必须是 2 的幂次方,否则可能会导致缓冲区溢出或者浪费内存等问题。同时,对于特殊的 channel 操作,如 close、select 和带缓冲区的 channel 等,也需要注意环形缓冲区的使用方式。
45. Go 方法与函数的区别?
在 Go 语言中,方法(method)是一个包含接收者参数的函数,用于为接收者类型提供一些行为。而函数(function)则是一段代码,可被调用并可接收参数和返回值。
方法需要被绑定到一个类型上,它们通过使用接收者参数来实现这一点。接收者可以是值类型或指针类型。值类型的接收者在方法执行时会将调用者的值复制一份,而指针类型的接收者则直接操作调用者的值,因此可以修改调用者的状态。
与方法不同,函数没有接收者参数,因此它们无法直接修改调用者的状态。函数在 Go 语言中是一等公民,可以像任何其他类型的值一样被传递和赋值。函数还可以是匿名的,或者被作为闭包使用,以便在不同的作用域中进行操作。
实例
在 Go 语言中,函数是一段代码块,可以独立调用,接受参数和返回结果,它没有任何属于对象的概念。而方法是和对象相关联的函数,它属于对象的一部分,可以调用对象的属性和方法。
例如,下面是一个函数和一个方法的示例:
// 函数
func add(x int, y int) int {
return x + y
}
// 方法
type Person struct {
Name string
Age int
}
func (p *Person) sayHello() {
fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
}
可以看到,函数 add
只是一个独立的代码块,而方法 sayHello
则是一个属于 Person
结构体对象的一部分。在调用方法时,需要先创建一个 Person
对象,然后通过这个对象调用方法,例如:
p := Person{Name: "Alice", Age: 30}
p.sayHello() // 输出:Hello, my name is Alice and I'm 30 years old.
总的来说,Go 语言中的方法与函数的区别在于方法需要绑定到一个类型上,并且可以直接修改调用者的状态,而函数则没有这些限制。