目录

Go面试题

go 面试题

New 和 Make 的区别

在 Go 语言中,New 和 Make 都是用来在内存中分配空间的函数,但是它们的作用却略有不同。

New 函数

New 函数用于在内存中分配一块空间,并返回该空间的地址。具体来说,New 函数返回一个指向新分配的零值的指针,这个指针指向的类型是传入参数的类型。例如,如果我们调用 var p *int = new(int),则会分配一个 int 类型的空间,并将其初始化为 0,然后返回这个空间的地址。

Make 函数

Make 函数用于在内存中分配并初始化一个 slice、map 或 channel 类型的对象。Make 函数的返回值类型与传入参数的类型相同。例如,如果我们调用 var slice []int = make([]int, 10),则会分配一个包含 10 个 int 类型元素的 slice,并返回这个 slice 的地址。注意,使用 Make 函数分配的对象都已经被初始化。

总结

  • New 函数返回一个指向新分配的零值的指针,它用于分配任何类型的内存空间。
  • Make 函数用于分配并初始化 slice、map 或 channel 类型的对象。

在 Go 语言中,内存管理是由垃圾回收器自动完成的。垃圾回收器会定期扫描内存,标记不再使用的对象,然后释放它们的内存。在代码中,我们不需要手动释放内存,因为编译器会自动在适当的时候释放它们。这种自动内存管理的方式可以避免常见的内存泄漏和空指针错误等问题。

当我们在 Go 函数中传递一个指针时,如果这个指针指向的数据被分配在栈上,那么这个指针就会逃逸到堆上,因为函数返回后栈上的空间就被释放了。在这种情况下,我们需要在堆上分配内存,并将指针指向这个内存空间,以确保这个指针在函数返回后仍然有效。如果指针指向的数据已经在堆上分配了空间,那么指针就不会逃逸。在 Go 中,编译器会根据上下文自动判断一个指针是否会逃逸,从而帮助我们优化代码的性能。

GMP调度原理

在 Go 语言中,GMP 表示 Goroutine、Schedule、Machine Processor。其中,Goroutine 是 Go 语言的轻量级线程,Schedule 是调度器,Machine Processor 是底层线程。

调度器的作用是将 Goroutine 分配到底层线程上执行,并确保 Goroutine 的公平性和高效性。调度器会根据 Goroutine 的状态和优先级等因素,决定将 Goroutine 调度到哪个底层线程上执行。

底层线程是 Go 语言的系统线程,由操作系统管理。底层线程会负责执行 Goroutine,当 Goroutine 遇到 I/O 阻塞或者调用了 runtime.Gosched() 函数时,底层线程会将 Goroutine 调度出去,让其他 Goroutine 继续执行。

在 GMP 调度原理中,Goroutine 是轻量级的,创建和销毁的成本很低。而底层线程则是重量级的,创建和销毁的成本很高。因此,Go 语言采用了一种 M:N 的线程模型,将多个 Goroutine 映射到少量的底层线程上执行,从而提高了并发性能和效率。

以上是关于 GMP 调度原理的简单介绍,如果您有更深入的问题或者需要更详细的解释,请随时提出。

linux线程有几种类型

Linux 中有两种类型的线程:用户级线程和内核级线程。用户级线程是通过用户库实现的,可以在应用程序中创建和管理。内核级线程是由操作系统内核创建和管理的,可以更好地利用系统资源和硬件设备。用户级线程通常比内核级线程更轻量级,但内核级线程可以更好地利用多核处理器和硬件加速器等硬件资源。

字符串函数名调用函数

在 Go 语言中,可以使用反射机制和字符串函数名调用函数。具体来说,我们可以使用 reflect 包中的 ValueOf 和 MethodByName 函数来获取函数的值和名称,然后使用 Call 函数调用该函数。例如,假设我们有一个函数 func Add(a, b int) int,我们可以使用以下代码调用该函数:

package main

import (
    "fmt"
    "reflect"
)

func Add(a, b int) int {
    return a + b
}

func main() {
    add := reflect.ValueOf(Add)
    args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
    res := add.Call(args)[0].Interface().(int)
    fmt.Println(res)
}

在这个示例中,我们首先使用 reflect.ValueOf 函数获取 Add 函数的值,然后使用 MethodByName 函数获取函数的名称。接下来,我们使用 Call 函数调用该函数,并将参数作为 reflect.Value 的切片传递。最后,我们使用 Interface 函数将返回值转换为 int 类型,并将其打印到控制台上。

需要注意的是,在使用 reflect 包调用函数时,我们需要确保传递的参数类型和数量与函数定义匹配,否则会导致运行时错误。因此,在编写代码时,请务必小心谨慎,并确保对函数参数有充分的了解。

go 锁模式

在 Go 语言中,锁是一种同步机制,用于保护共享资源免受并发访问的影响。Go 语言中提供了多种锁模式,包括互斥锁、读写锁、条件变量等。

互斥锁是一种基本的锁模式,用于保护共享资源免受并发访问。互斥锁具有两种状态:已锁定和未锁定。在任何时候,只有一个 Goroutine 可以持有互斥锁。如果另一个 Goroutine 尝试获取已锁定的互斥锁,它将会被阻塞,直到互斥锁被释放为止。

读写锁是一种高效的锁模式,它允许多个 Goroutine 同时读取共享资源,但只允许一个 Goroutine 写入共享资源。读写锁具有两种状态:读取模式和写入模式。在读取模式下,多个 Goroutine 可以同时读取共享资源,但不能写入。在写入模式下,只有一个 Goroutine 可以写入共享资源,同时禁止任何读取操作。如果一个 Goroutine 正在写入共享资源,那么所有读取和写入操作都将被阻塞,直到写入操作完成为止。

条件变量是一种高级的锁模式,它允许 Goroutine 在共享资源达到某个状态时等待通知。条件变量通常与互斥锁一起使用,以确保 Goroutine 在访问共享资源之前获得锁。一旦 Goroutine 获得了锁,它就可以检查共享资源的状态,并使用 Wait 函数等待条件变量达到所需状态。当共享资源达到所需状态时,Goroutine 可以使用 Signal 或 Broadcast 函数通知等待的 Goroutine,以便它们可以重新检查共享资源并继续执行。

以上是 Go 语言中常用的锁模式,您可以根据具体的需求选择适合的锁模式。需要注意的是,在使用锁时,请务必小心谨慎,并确保正确地处理锁的释放,以避免死锁和其他并发问题的出现。

go 通道

在 Go 语言中,通道(Channel)是一种用于 Goroutine 通信和同步的强大工具。通道是一种类型化的管道,通过通道操作符 <- 可以发送和接收值。要创建通道,可以使用内置的 make 函数,例如:ch := make(chan int)

默认情况下,通道是非缓冲的,这意味着向通道发送数据时,如果没有 Goroutine 准备好接收数据,发送操作就会被阻塞。而缓冲通道具有固定的容量,并且可以在通道已满之前发送和接收值,不会被阻塞。

通道可以用于实现各种同步模式,例如锁、信号量和监视器。它们尤其适用于协调对共享资源的并发访问以及在 Goroutine 之间实现消息传递。

要向通道发送值,可以使用语法 ch <- value,这将阻塞,直到另一个 Goroutine 准备好从通道接收值。要从通道接收值,可以使用语法 value := <-ch,这将阻塞,直到另一个 Goroutine 向通道发送值。

除了发送和接收值之外,通道还可以使用内置的 close 函数关闭。当通道关闭时,任何因在通道上阻塞而等待的 Goroutine 都将被解除阻塞并接收到零值。

总体而言,通道是 Go 语言中用于并发编程的强大且灵活的工具。它们提供了一种安全而有效的方式来在 Goroutine 之间通信和同步,并可用于实现各种同步模式和通信协议。

在 MySQL 数据库中,有多种类型的锁可用于保护数据免受并发访问的影响。以下是 MySQL 中常用的两种锁类型:

  • 共享锁(Shared Lock):共享锁用于保护数据免受读取操作的干扰。共享锁可以被多个事务共享,因此多个事务可以同时读取同一组数据。共享锁可以通过使用 SELECT ... FOR SHARESELECT ... LOCK IN SHARE MODE 语句来获取。
  • 排他锁(Exclusive Lock):排他锁用于保护数据免受写入操作和其他排他锁的干扰。排他锁只能由一个事务持有,并且在事务持有锁时,其他事务无法读取或写入相同的数据。排他锁可以通过使用 SELECT ... FOR UPDATEUPDATE ... 语句来获取。

除了共享锁和排他锁之外,MySQL 还提供了其他类型的锁,例如行锁和表锁等。这些锁的使用方式和特性各不相同,您可以根据具体的需求选择适合的锁类型。

需要注意的是,在使用锁时,请务必小心谨慎,并确保正确地处理锁的释放,以避免死锁和其他并发问题的出现。另外,如果您需要在 MySQL 数据库中使用锁,建议您在进行大规模并发操作之前,先进行充分的测试和评估,以确保您的应用程序能够在实际情况下正确地处理锁和并发访问。

redis实现分布式锁

在 Redis 中,可以使用 SETNX 命令实现分布式锁。具体来说,我们可以将锁作为一个字符串类型的键写入 Redis 中。如果键不存在,就将其设置为锁定状态,并设置一个过期时间。在加锁时,我们需要使用 SETNX 命令来尝试获取锁。如果 SETNX 命令返回 1,表示锁定成功,否则表示锁已经被其他客户端获取。

为了防止锁被长时间锁定,我们还需要设置一个过期时间来自动释放锁。在释放锁时,我们可以使用 DEL 命令将锁从 Redis 中删除。

以下是一个示例代码,演示如何使用 Redis 实现分布式锁:

package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "time"
)

func main() {
    client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })

    // 加锁
    lockKey := "my_lock"
    lockValue := "my_value"
    lockExpire := 5 * time.Second
    locked, err := client.SetNX(lockKey, lockValue, lockExpire).Result()
    if err != nil {
        panic(err)
    }
    if !locked {
        fmt.Println("failed to acquire lock")
    } else {
        fmt.Println("acquired lock")
    }

    // 释放锁
    _, err = client.Del(lockKey).Result()
    if err != nil {
        panic(err)
    }
    fmt.Println("released lock")
}

在这个示例中,我们首先创建了一个 Redis 客户端,并使用 SETNX 命令获取锁。如果 SETNX 命令返回 1,表示锁定成功,否则表示锁已经被其他客户端获取。我们还设置了一个过期时间来自动释放锁。

接下来,我们使用 DEL 命令将锁从 Redis 中删除,释放锁。需要注意的是,在使用 Redis 分布式锁时,请务必遵循最佳实践,例如设置适当的过期时间、使用唯一的锁名称等,以避免并发问题的出现。