Golang面试题

1,335 阅读19分钟

map不初始化会怎么样?

在 Golang 中,如果您声明了一个 map 变量但没有初始化它,那么这个 map 变量的值为 nil,而 nil 的 map 不能直接使用,任何尝试读取或写入它的操作都会导致运行时 panic。

以下是一个示例代码,展示了尝试在未初始化的 map 变量上进行写入操作的情况:

goCopy code
var myMap map[string]int

myMap["one"] = 1  // 将导致 panic: assignment to entry in nil map

为了避免这种情况,您可以使用 make 函数来初始化 map:

goCopy code
myMap := make(map[string]int)

或者您可以在声明时直接初始化 map 变量:

goCopy code
myMap := map[string]int{"one": 1, "two": 2}

这样就可以避免在使用 map 变量时出现 nil 引用的错误。

map 不初始化长度和初始化长度的区别?

在Go语言中,map 是一种数据结构,用于存储键值对。当你声明一个 map 时,你可以选择是否初始化它的长度。

如果你不初始化 map 的长度,那么它的初始长度为 0,也就是说它是空的。当你向一个空的 map 中添加元素时,map 会自动扩容以适应新的键值对。这种方式可以在一开始无法确定需要存储多少元素时使用,或者当你不想手动指定长度时使用。

示例代码:

goCopy code
// 声明一个空的 map
var myMap map[string]int

// 添加键值对到空的 map 中
myMap["a"] = 1
myMap["b"] = 2

在上面的代码中,我们没有初始化 myMap 的长度,因此它是一个空的 map。我们可以通过将键值对添加到 myMap 中来动态地扩展它。

如果你知道你的 map 需要存储多少个键值对,你可以初始化 map 的长度。这样可以避免在添加元素时不断扩容 map,从而提高性能。但是,这种方式只适用于你已经知道了你的 map 将要存储的元素数量。

示例代码:

goCopy code
// 初始化长度为 10 的 map
myMap := make(map[string]int, 10)

// 添加键值对到 map 中
myMap["a"] = 1
myMap["b"] = 2

在上面的代码中,我们使用 make() 函数初始化了长度为 10 的 map。这样,当我们向 map 中添加键值对时,map 不需要进行扩容。

总结:初始化的不会进行扩容,能够提高性能。不初始化的是空map,会自动扩容。扩容操作就会消耗性能。

map 承载多大,大了怎么办

在 Go 语言中,map 的大小取决于可用的内存大小。这意味着,如果你的系统有足够的内存,那么 map 的大小理论上是没有限制的。

然而,当 map 存储大量的键值对时,可能会出现性能问题。因为当 map 被填满时,会自动扩容并重新哈希。这个过程需要重新分配内存并重新哈希每个键,因此会导致性能下降。因此,尽可能地预估 map 的大小,并在初始化时指定它的容量,可以提高性能。

当 map 已经很大且无法继续扩容时,你可以考虑使用其他数据结构,例如数据库或者分布式缓存,来存储大量的键值对。同时,你也可以优化你的算法或代码,以减少 map 中键值对的数量。

如果你需要使用非常大的 map,也可以考虑使用 Go 语言提供的一些专门针对大规模数据处理的框架和库,例如 Apache Arrow 或者 Apache Parquet。

总之,Go 语言中的 map 是一种非常强大和方便的数据结构,可以承载大量的键值对。但是,在处理大量数据时,你需要预估 map 的大小,以及考虑使用其他数据结构或优化算法来提高性能。

总结:map理论上是内存有多大,就可以存多大,但是扩容的时候性能就会下降,可以考虑使用GO语言提供的一些专门针对大规模数据处理的框架和库。例如:apache arrow 或者 Apache parquet

map 的 iterator 是否安全?能不能一边 delete 一边遍历?

在 Go 语言中,遍历 map 的迭代器是安全的,可以在遍历时进行并发的读操作。但是在迭代期间对 map 进行写操作(包括添加、删除键值对等操作)是不安全的,可能导致遍历出现不可预期的结果。

因此,当你需要对 map 进行迭代和写操作时,你需要采取一些措施来确保并发安全。具体来说,你可以使用 Go 语言提供的 sync 包中的 RWMutexMutex 来保护 map 的读写操作。在进行写操作时,你需要获得写锁,以避免并发写操作。在进行读操作时,你可以获得读锁,以允许并发读操作。以下是一个示例代码:

goCopy code
// 声明一个 map
myMap := make(map[string]int)

// 用 Mutex 来保护 map
var mu sync.Mutex

// 写操作需要获取锁
mu.Lock()
myMap["a"] = 1
mu.Unlock()

// 读操作可以同时进行
mu.Lock()
for key, value := range myMap {
    fmt.Println(key, value)
}
mu.Unlock()

在上面的代码中,我们使用了 sync.Mutex 来保护 map 的读写操作。当进行写操作时,我们获取了写锁。当进行读操作时,我们获取了读锁。这样可以确保 map 在并发访问时的安全性。

需要注意的是,在迭代 map 的过程中,如果需要删除元素,你需要先记录要删除的键,然后在迭代完成后再进行删除操作。这是因为删除元素会影响 map 的长度和哈希表的状态,可能导致迭代器失效。

总之,遍历 map 的迭代器是安全的,但在迭代期间对 map 进行写操作是不安全的。如果需要在迭代期间对 map 进行写操作,你需要采取措施来确保并发安全。

golang中字符串不能改,那转成数组能改吗,怎么改

在 Go 语言中,字符串是不可变的。这意味着,一旦字符串被创建,就不能再进行修改。但是,你可以将字符串转换为字节数组来进行修改。字节数组是可变的,你可以在其中进行添加、删除、修改元素等操作。

在 Go 语言中,可以使用 []byte 类型将字符串转换为字节数组。转换后,你可以对字节数组进行修改,然后再将其转换回字符串类型。以下是一个示例代码:

goCopy code
str := "hello"
// 将字符串转换为字节数组
bytes := []byte(str)
// 修改字节数组中的元素
bytes[0] = 'H'
// 将字节数组转换回字符串类型
str = string(bytes)
fmt.Println(str) // 输出 "Hello"

在上面的代码中,我们首先将字符串 "hello" 转换为字节数组 bytes。然后,我们将字节数组中的第一个元素修改为大写字母 'H'。最后,我们将修改后的字节数组转换回字符串类型,并输出结果。

需要注意的是,在将字符串转换为字节数组后,你需要使用单引号来表示字符,而不是双引号。因为在 Go 语言中,双引号表示字符串,而单引号表示字符。此外,你还需要注意在进行字符串和字节数组之间的转换时,涉及到字符集和编码的问题,可能会影响到转换后的结果。

总之,你可以将字符串转换为字节数组来进行修改,但需要注意在转换时的字符集和编码问题,并最终将修改后的字节数组转换回字符串类型。

golang中怎么判断一个数组是否已经排序

在 Go 语言中,可以使用以下方法来判断一个数组是否已经排序:

  1. 手动遍历数组:可以通过遍历数组,判断当前元素是否小于等于下一个元素,来判断数组是否已经排序。如果所有元素都符合这个条件,那么数组已经排序。示例代码如下:
goCopy code
func isSorted(arr []int) bool {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        if arr[i] > arr[i+1] {
            return false
        }
    }
    return true
}

在上面的代码中,我们首先获取数组的长度 n,然后遍历数组,判断当前元素是否大于下一个元素。如果当前元素大于下一个元素,说明数组没有排序,返回 false。如果所有元素都符合条件,说明数组已经排序,返回 true

  1. 使用内置函数:Go 语言提供了 sort.Ints 函数来对整数数组进行排序。如果排序后的数组与原始数组相等,那么说明原始数组已经排序。示例代码如下:
goCopy code
func isSorted(arr []int) bool {
    n := len(arr)
    sortedArr := make([]int, n)
    copy(sortedArr, arr)
    sort.Ints(sortedArr)
    return reflect.DeepEqual(arr, sortedArr)
}

在上面的代码中,我们首先使用 make 函数创建一个长度为 n 的新数组 sortedArr,然后将原始数组 arr 的值复制到新数组中。接着,我们使用 sort.Ints 函数对新数组进行排序。最后,我们使用 reflect.DeepEqual 函数比较原始数组和排序后的数组是否相等。如果相等,说明原始数组已经排序。

需要注意的是,在使用 reflect.DeepEqual 函数进行比较时,需要保证两个数组的类型和元素类型相同,否则可能导致比较结果不准确。

总之,可以通过手动遍历数组或使用内置函数来判断一个数组是否已经排序。手动遍历数组可以自定义判断条件,而使用内置函数可以简化代码。

golang中普通 map 如何不用锁解决协程安全问题

使用 sync.map 处理

golang中array和slice的区别

  1. 长度固定 vs 长度可变:array 的长度是固定的,一旦定义后无法再修改;而 slice 的长度是可变的,可以动态增加或减少。
  2. 传递方式不同:array 是值类型,它在函数间传递时会进行复制,因此对其进行修改不会影响原数组的值;而 slice 是引用类型,它在函数间传递时传递的是指针,对其进行修改会影响原 slice 的值。
  3. 底层数据结构不同:array 的底层是一个连续的内存块,元素的类型和大小都是相同的;而 slice 的底层是一个指向底层数组的指针、长度和容量的结构体,因此可以存储不同类型的元素,且可以动态扩容。
  4. 初始化方式不同:array 可以使用 { } 初始化,也可以使用 var 定义并指定长度进行初始化;而 slice 只能使用 make 函数或者字面量初始化。

golang 中 json包变量不加tag会怎么样

在 Go 语言的 encoding/json 包中,如果一个 struct 中的字段没有指定 json tag(即 struct tag 中没有指定 json:"..."),则在将该 struct 序列化为 JSON 字符串时,会使用该字段的名称作为 JSON 对象中的键。

例如,以下代码中定义了一个结构体 Person,其中包含了三个字段 NameAgeAddress。其中 Name 字段指定了 json tag json:"name"Address 字段没有指定 json tag。当将该 struct 序列化为 JSON 字符串时,Name 字段的值会被序列化到 name 键下,Address 字段的值会被序列化到 Address 键下。

goCopy code
package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name    string `json:"name"`
    Age     int
    Address string
}

func main() {
    p := Person{Name: "Alice", Age: 20, Address: "Somewhere"}
    b, err := json.Marshal(p)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(b)) // 输出:{"name":"Alice","Age":20,"Address":"Somewhere"}
}

如果我们将 Address 字段的定义改为 string 类型而不是 Address string,则该字段没有任何名称,也没有指定 json tag。在这种情况下,序列化后的 JSON 对象中将不会包含该字段。例如:

goCopy code
package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name    string `json:"name"`
    Age     int
    string
}

func main() {
    p := Person{Name: "Alice", Age: 20, string: "Somewhere"}
    b, err := json.Marshal(p)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(b)) // 输出:{"name":"Alice","Age":20}
}

在上面的代码中,Address 字段被改为匿名的 string 类型。因此,该字段没有任何名称,也没有指定 json tag。在将该 struct 序列化为 JSON 字符串时,序列化后的 JSON 对象中不包含该字段。

golang中零切片、空切片和nil切片是什么?

在 Go 语言中,有三种空的 slice:零切片、空切片、和 nil 切片。

  1. 零切片:声明一个 slice 变量,但没有初始化它,那么该变量就是一个零值(默认值)为 nil 的 slice。可以通过 var s []int 的方式声明一个零切片。零切片不能用于添加元素,必须使用 make 函数来初始化它。
  2. 空切片:已经初始化的 slice,但其中不包含任何元素。可以通过 make 函数来创建一个指定长度的空切片,例如:s := make([]int, 0)
  3. nil 切片:未初始化的 slice,也可以通过 var s []int 的方式声明一个 nil 切片。与零切片一样,nil 切片不能用于添加元素,必须使用 make 函数来初始化它。

区别:

  • 零切片和 nil 切片的值都为 nil,它们在使用时需要先通过 make 函数进行初始化,才能添加元素。
  • 空切片的长度为 0,但它已经被初始化了,因此可以直接添加元素。

slice 深拷贝和浅拷贝

在 Go 语言中,slice 是一个引用类型,所以在进行赋值或传递参数时,只会复制 slice 的指针,而不是复制整个底层数组。这就导致了在进行 slice 的操作时,可能会产生意想不到的结果。为了解决这个问题,可以使用 slice 的深拷贝和浅拷贝。

浅拷贝:

浅拷贝是指将一个 slice 赋值给另一个 slice,这两个 slice 共享同一个底层数组。因此,如果修改了其中一个 slice 的元素,另一个 slice 的元素也会随之改变。例如:

goCopy code
package main

import "fmt"

func main() {
    // 创建一个 slice
    s1 := []int{1, 2, 3}

    // 浅拷贝 s1
    s2 := s1

    // 修改 s2 的第一个元素
    s2[0] = 100

    // s1 的第一个元素也被修改了
    fmt.Println(s1) // 输出 [100 2 3]
}

深拷贝:

深拷贝是指将一个 slice 复制到一个新的 slice 中,两个 slice 使用不同的底层数组。因此,如果修改其中一个 slice 的元素,另一个 slice 的元素不会随之改变。可以通过使用内置的 copy 函数来实现 slice 的深拷贝。例如:

goCopy code
package main

import "fmt"

func main() {
    // 创建一个 slice
    s1 := []int{1, 2, 3}

    // 深拷贝 s1
    s2 := make([]int, len(s1))
    copy(s2, s1)

    // 修改 s2 的第一个元素
    s2[0] = 100

    // s1 的第一个元素不受影响
    fmt.Println(s1) // 输出 [1 2 3]
    fmt.Println(s2) // 输出 [100 2 3]
}

总结:

  • slice 是一个引用类型,赋值或传递参数时只会复制 slice 的指针。
  • 对 slice 进行浅拷贝会导致两个 slice 共享同一个底层数组,对其中一个 slice 进行操作会影响另一个 slice。
  • 对 slice 进行深拷贝会创建一个新的 slice,并复制底层数组的值,两个 slice 使用不同的底层数组。

golang map触发扩容的时机,满足什么条件时扩容?扩容的策略是什么?

在 Go 语言中,map 内部实现使用哈希表,当元素数量达到一定阈值时,就会触发 map 扩容。具体来说,当 map 中元素数量达到哈希表长度的 2/3 时,就会触发扩容操作。

map扩容的策略是

  1. 当元素数量达到哈希表长度的 2/3 时,会触发扩容操作。
  2. 扩容时,会重新计算哈希表的长度,并重新分配内存。
  3. 新的哈希表长度必须是 2 的幂次方,且大于等于元素数量的两倍。
  4. 扩容完成后,会将旧的键值对重新哈希到新的哈希表中。

golang 中slice ,map,chanel 创建的时候的几个参数什么含义

Slice

在创建一个 slice 时,可以使用 make 函数来指定其长度和容量,语法如下:

rCopy code
make([]T, length, capacity)

其中,T 表示 slice 中元素的类型,length 表示 slice 的长度(即其中元素的个数),capacity 表示 slice 底层数组的容量。slice 底层数组的容量是指该数组能够容纳的最大元素数量,容量可以省略,此时默认长度和容量相等。

Map

在创建一个 map 时,可以使用 make 函数来指定其初始容量,语法如下:

goCopy code
make(map[T]V, capacity)

其中,T 表示键的类型,V 表示值的类型,capacity 表示 map 的初始容量。map 的容量是指其内部存储结构的大小,可以根据实际情况来预估或者动态调整。

Channel

在创建一个 channel 时,可以使用 make 函数来指定其缓冲区大小,语法如下:

goCopy code
make(chan T, capacity)

其中,T 表示 channel 中元素的类型,capacity 表示 channel 缓冲区的大小。如果缓冲区大小为 0,则表示该 channel 是无缓冲的,任何写操作都会被阻塞,直到有读操作读取数据。如果缓冲区大小大于 0,则表示该 channel 是带缓冲的,可以缓存一定数量的元素,直到缓冲区满时,写操作才会被阻塞。

golang中对已经关闭的chan进行读写,会怎么样?为什么?

在Golang中,对于已经关闭的通道进行读取操作,会立即返回通道类型的零值,而不会发生阻塞等待通道中的数据。如果对已经关闭的通道进行写入操作,则会导致panic异常。这是因为关闭通道后,通道的状态被设置为“关闭状态”,意味着已经不再接受新的数据,并且已经无法再将数据写入通道中,任何尝试写入数据的操作都会导致panic异常。例如:

goCopy code
package main

func main() {
    ch := make(chan int)
    close(ch)
    // 读取一个已经关闭的通道
    i := <-ch
    fmt.Println(i) // 输出0
    // 向一个已经关闭的通道写入数据
    ch <- 1 // panic: send on closed channel
}

在这个示例中,创建了一个整数通道并将其关闭。然后,尝试从通道中读取一个整数值,这会立即返回0,因为通道已经关闭。接下来,尝试将一个整数值写入通道,这会导致panic异常,因为通道已经关闭。

因此,应该避免在已经关闭的通道上进行写操作,并且在读取通道数据之前,应该使用ok参数检查通道是否已经关闭,以避免在关闭的通道上读取到通道类型的零值。

golang中对一个未初始化的chan进行读写,会怎么样?为什么

在Golang中,对于一个未初始化的通道进行读取或写入操作,会导致编译时错误。因为未初始化的通道值是nil,不能进行任何读取或写入操作。例如:

goCopy code
package main

func main() {
    var ch chan int
    // 向未初始化的通道写入数据
    ch <- 1 // 编译时错误:use of untyped nil
    // 从未初始化的通道读取数据
    i := <-ch // 编译时错误:use of untyped nil
    fmt.Println(i)
}

在这个示例中,创建了一个未初始化的整数通道ch。然后尝试将一个整数值写入通道,这会导致编译时错误。同样,尝试从通道中读取一个整数值,也会导致编译时错误。

因此,在使用通道之前,必须先对其进行初始化。可以使用make函数对通道进行初始化。例如:

goCopy code
package main

func main() {
    ch := make(chan int)
    // 向通道写入数据
    ch <- 1
    // 从通道读取数据
    i := <-ch
    fmt.Println(i)
}

在这个示例中,使用make函数初始化了一个整数通道ch,然后可以安全地向通道写入数据并从通道中读取数据。

golang中sync.map 的优缺点和使用场景

sync.Map是Go语言提供的一种并发安全的Map类型,它是在Go 1.9版本中引入的。与标准库中的map类型不同,sync.Map是并发安全的,可以被多个goroutine同时读写,而不需要额外的锁机制,因此能够大大提高程序的并发性能。sync.Map内部使用了一种特殊的技术实现,可以将数据分散到多个小的Map中,这些小的Map可以被多个goroutine同时访问,以此来减少竞争的机会。

优点:

  • 并发安全:可以被多个goroutine同时读写,无需额外的锁机制,提高程序的并发性能。
  • 高效:内部使用了特殊的技术实现,可以将数据分散到多个小的Map中,减少竞争的机会。
  • 动态增长:在需要的时候,可以动态地增加Map的大小,而无需担心锁竞争问题。

缺点:

  • 空间浪费:由于sync.Map内部使用了多个小的Map,因此可能会造成一定的空间浪费。
  • 不支持迭代器:sync.Map不支持迭代器,因此无法对Map进行遍历操作,需要使用Range()函数对Map进行遍历操作。
  • 不支持原子性操作:虽然sync.Map是并发安全的,但是它不支持原子性操作,如果需要执行原子性操作,仍然需要使用sync/atomic包提供的原子性操作。

使用场景:

  • 适用于多个goroutine并发读写的场景,能够提高程序的并发性能。
  • 适用于需要动态增长Map大小的场景,能够提高程序的灵活性和可维护性。
  • 适用于数据访问不太频繁的场景,由于sync.Map内部需要使用特殊的技术实现,因此在数据访问频繁的场景下可能会影响程序的性能。
  • 适用于数据访问并不需要使用迭代器的场景,如果需要使用迭代器遍历Map,建议使用标准库中的map类型。