面试题

什么是指针和指针变量?

普通变量存储数据,而指针变量存储的是数据的地址。

  • 学习指针,主要有两个运算符号&*

    1. &:地址运算符,从变量中取地址

    2. *:引用运算符,取地址中数据

num := 99
fmt.Println(num) //输出: 99

ptr := &num
fmt.Println(ptr) //输出: 例如:0xc000086020

tamp := *ptr
fmt.Println(tamp) //输出: 99

为什么使用指针?

意义一:容易编码

指针在数据结构中起着重要的作用。通过指针,我们可以创建复杂的数据结构,如链表、树和图。指针可在数据结构中轻松地访问和操作节点之间的关系,从而实现高效的数据存储和检索。

指针可在函数之间传递数据的引用,而不是复制整个数据。这样可以节省内存空间,并提高程序的执行效率。通过传递指针,函数可以直接修改原始数据,而不需要返回值。

意义二:节省内存

指针可直接访问和修改内存中的数据,通过指针,我们可以在运行时动态地分配内存,以满足程序的需求,并在不需要时释放内存,避免内存泄漏。

指针可在程序运行时动态地分配内存。通过动态内存分配,我们可以根据需要分配和释放内存,从而提高程序的灵活性和效率。

哪些对象可以获取地址,哪些不行?

可以使用 & 获取内存地址的对象:

  • 变量

  • 指针

  • 数组,切片及其内部数据

  • 结构体指针

  • Map

不能寻址的对象:

  • 结构体

  • 常量

  • 字面量

  • 函数

  • map 非指针元素

  • 数组字面量

未初始化的 Map 可以读取 key 吗?

可以的,未执行 make 初始化的 map 读取任何 key 都会返回当前类型的空值

package main

import "fmt"

func main() {
	var m map[int]int

	fmt.Println(m[1])
}

// 结果:
// 0

如果对未初始化的 Map 赋值会怎么样?

会触发 panic 异常错误

package main

func main() {
	var m map[int]int

	m[1] = 1
}

// 结果:
// panic: assignment to entry in nil map

如果对未初始化的 Map 进行删除 key 的操作会发生什么?

早期如果对未初始化的 map 进行 delete 操作会报 panic 错误, 现在的版本对于未初始化的 map 进行 delete 是不会报错的。

package main

func main() {
	var m map[int]int

	delete(m, 1)
}

// 结果:
//

数组和切片有什么区别?

  • 数组的长度是固定的,在创建的时候就已经确定,且不可改变。切片的长度是动态的,会根据添加的数据自动扩容。

  • 在函数参数传递时数据是值传递,切片是引用传递

  • 切片有容量 (capacity) 参数,数组没有

如果 for range 同时添加数据, for range 会无限执行吗?

不会,在执行 for range 的时候实际遍历的是变量的副本,所以改变遍历的变量是不会有影响的

package main

import "fmt"

func main() {
	n := []int{1, 2, 3}

	for  _, v := range n {
		n = append(n, v)
	}

	fmt.Println(n) // 结果: [1 2 3 1 2 3]
}

多个 defer 的执行顺序是什么?

执行的顺序类似堆栈,先进后出

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println(1)
	}()

	defer func() {
		fmt.Println(2)
	}()

	defer func() {
		fmt.Println(3)
	}()
}

// 结果:
// 3
// 2
// 1

什么是数据溢出?

在使用数字类型时如果数据达到最大值,则接下来的数据将会溢出,如 uint 溢出后会从 0 开始, int 溢出后会变为负数。

package main

import (
	"fmt"
	"math"
)

func main() {
	var n int8 = math.MaxInt8
	var m uint8 = math.MaxUint8

	n += 2
	m += 1

	fmt.Println(n) // -127
	fmt.Println(m) // 0
}

如何避免?

  • 正数优先使用 uint, 范围更大

  • 添加判断代码判断是否溢出

函数参数使用值还是指针?

  • 值传递

一般来说,对于常见的类型都可以使用值传递,值传递的优点是函数内对值的修改不会影响原始的变量,也不会出现并发问题。缺点是值传递会复制一份对应变量的副本,对内存占用会多一些,如果传入的结构体非常大,使用值传递就不太合适。

  • 指针和引用传递

使用指针传递的好处是直接传递变量的地址,不需要额外的空间,缺点是并发操作时数据修改会影响到原始的数据。传入切片实际上就是传递切片的指针,避免重复拷贝,若传入数组则是值传递,会拷贝一份。

Golang 常见的字符串拼接方式有哪些?效率有何不同?

方法
描述

+

使用 + 操作符进行拼接会对遍历字符串,计算并开辟一个新的空间来存储合并后的字符串

fmt.Sprintf

由于 printf 中可以使用 %d 等表示变量类型, sprintf 需要使用到反射来将不同的类型进行转换,效率较低

strings.Builder

使用 WriteString() 进行拼接操作,内部使用 []byte 切片和 unsafe.pointer 指针实现

bytes.Buffer

byte 缓冲器,底层是 []byte 切片

strings.Join

strings.Join 是基于 strings.Builder 来实现的,在 Join 方法内调用了 b.Grow(n) 方法, 预分配了内存空间,较为高效

strings.Builderbytes.Buffer 有什么区别?

  1. strings.Builder 会预分配空间,减少扩容,效率更高,适合较长的字符串拼接操作

  2. bytes.Buffer 主要用于处理单个字符,拥有许多针对单个 byte 的操作,如删除替换等,这个是 strings.Builder 没有的。

效率排行 strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf

使用过 context 吗? context 有哪些使用场景?

场景
介绍

超时处理

通过使用 context 可以方便地设置超时时间,在超时后自动终止协程

终止协程

通过使用 cancel() 方法,协程可以很方便地终止

传递数据

我们可以将数据写入 context, 在不同协程间传递数据

channel 是线程安全的吗?

channel 是线程安全的,原因是 channel 内部实现了锁的机制,

Map 使用 range 遍历时是有序还是无序的?

Map 在内部使用哈希算法放置元素,在自动扩容时又会重新计算哈希值,因此元素的地址会不断变化,官方为了避免用户认为 Map 元素排列是有序的,直接采用随机顺序返回,所以遍历是无序的。

Map 并发安全吗?

Map不能保证并发安全

要保证并发安全,使用以下方式:

  • 手动加读写锁

  • 使用 sync.Map

Map 的 key 删除后 key 的内存会被释放吗?

若 map 的 value 为

  • 值类型 (int uint float32 string struct{}...), 则 key 被删除后 value 不会被内存回收

  • 引用类型 (map slices chan ...), 则 key 被删除后 value 会被内存回收

如果我们想强制回收,如何操作?

  • 将 map 设置为 nil

  • 将 map 需要保留的值放置到一个新的 map 并赋值给当前的 map

Map 产生的 panic 异常能被 recover 吗?

Map 由于并发读写导致的 panic 是不能被 recover 的,因为 Map 的异常使用 runtime.throw() 抛出,这类异常不能被 recover。

if h.flags&hashWriting != 0 {
  throw("concurrent map read and map write")
}

最后更新于