欢迎光临
我们一直在努力

Golang面试题库(基础相关)

文章目录

  • 1. 相比较于其他语言,Go 有什么优势或者特点?
    • 回答
  • 2. go是面向对象的吗
    • 回答:
  • 3. golang 中 make 和 new 的区别?(基本必问)
    • 回答
    • 面试常见追问:为什么 new(map) 不如 make(map)?
  • 4. 数组和切片的区别(基本必问),切片怎么扩容
    • 回答
    • 切片怎么扩容?
      • 回答
    • 为什么代码中测试扩容大小和策略对不上?
  • 5. for i, v := range 切片,v地址会变化吗
    • 回答
    • for range 循环遍历 slice 有什么问题?
  • 6. go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?(defer 和 return)
    • 6.1 defer 的执行顺序
    • 回答
    • 6.2 defer在什么时机会修改返回值
    • 回答
    • defer recover 的问题?(主要是能不能捕获)
  • 7. uint 类型溢出
  • 8. 介绍 rune 类型
    • 回答
  • 9. Go中两个Nil可能不相等吗?
    • 回答​
  • 10. golang 中解析 tag 是怎么实现的?反射原理是什么?(问的很少,但是代码中用的多)
    • 解析 tag
    • 回答
    • 反射的实现原理
      • 回答
  • 11. go struct 能不能比较?
    • 回答
  • 12. 结构体打印时,%v 和 %+v 的区别
    • 回答
  • 13. 空 struct{} 占用空间么?
    • 回答
  • 14. go语言中空 struct{} 的用途?
    • 1. **将 map 作为集合(Set)使用时**,可以将值类型定义为空结构体,仅作为占位符使用即可。
    • 2. 不发送数据的信道(channel)
    • 3. 结构体只包含方法,不包含任何的字段
    • 回答
  • 15. go中"_"的作用
    • 回答
  • 16. Go 闭包
    • 回答
  • 17. [Go 多返回值怎么实现的?](https://go.cyub.vip/function/call-stack.html)
    • 回答
    • SP栈指针
    • FP帧指针
  • 18. Go 语言中不能比较的类型如何比较是否相等?
    • 回答
  • 19. Go 中 init 函数的特征?
    • 回答
  • 20. Go 中 uintptr 和 unsafe.Pointer 的区别?
    • 回答

1. 相比较于其他语言,Go 有什么优势或者特点?

分析 这是一个开放性题目,言之有理即可,主要考察对go语言整体的一个理解和感受,看看对go语言了解是否全面,可以从语法层面,是否支持跨平台编译,以及对并发编程的支持方式以及协程支持等几个方面来分析

回答

  • Go 允许跨平台编译,编译出来的是二进制的可执行文件,直接部署在对应系统上即可运行。
  • Go 在语言层次上天生支持高并发,通过 goroutine 和 channel 实现。channel 的理论依据是 CSP 并发模型【CSP 指的是 Communicating Sequential Processes(通过通信来共享数据的并发模型)】,即所谓的 通过通信来共享内存;Go 在 runtime 运行时里实现了属于自己的调度机制:GMP,降低了内核态和用户态的切换成本。
  • Go 的语法简单,代码风格比较统一

2. go是面向对象的吗

分析: Go官网的回答中提到,Yes or No. 也就是说Go不是面向对象语言,但也可以进行面向对象风格的编程,Go可以看作是一种泛化的面向对象,他不像Java那样那么规范,Go的对象没有层次结构,但也使得Go的对象比Java中的对象更轻量级

在这里插入图片描述

没有层次结构指的是这意味着Go不支持传统面向对象意义上的继承,而是更喜欢专注于组合。但是我们可以使用接口、结构体和嵌入在Go中模拟继承。不过,以这种方式创建复杂的继承层次结构是非常笨拙的。

这里可以引发一下思考,不是说面向对象才是最优的代码结构和开发范式,各有优缺点

回答:

我之前有阅读过Go官方QA文档,答案是Yes and No,也就是说Go不是面向对象语言,但也可以进行面向对象风格的编程,在Go里面,实现面向对象三大特性是这样的

封装: Go语言里面字段首字母大写来决定字段是否可以被外界访问 封装: 在一个对象内部,某些方法和数据可以是私有的,不能被外界访问,封装为对象内部数据提供了不同级别的保护

继承: Go语言里面用组合结构体的方式 或 接口继承来实现 继承: 让某个类型的对象获得另一个类型的对象的属性 继承概念的实现方式有二类:实现继承与接口继承。

  • 实现继承就是直接使用父类的属性和方法而无需额外编码的能力

package main

import "fmt"

type Base struct {
Name string
}

func (b Base) Hello() {
fmt.Println("hello,", b.Name)
}

// 子结构体“继承”Base:嵌入 Base
type User struct {
Base // 匿名字段:嵌入
Age int
}

func main() {
u := User{
Base: Base{Name: "Tom"},
Age: 18,
}

// 继承字段
fmt.Println(u.Name) // Tom(等价于 u.Base.Name)

// 继承方法
u.Hello() // hello, Tom(等价于 u.Base.Hello())
}

  • 接口继承是指仅使用属性和方法的名称,但是子类必须提供实现的能力。

package main

import "fmt"

type Reader interface {
Read() string
}

type Writer interface {
Write(s string)
}

// ReadWriter “继承”了 Reader + Writer
type ReadWriter interface {
Reader
Writer
}

type File struct {
data string
}

func (f *File) Read() string {
return f.data
}

func (f *File) Write(s string) {
f.data = s
}

func main() {
var rw ReadWriter = &File{}
rw.Write("hello")
fmt.Println(rw.Read()) // hello
}

多态: Go语言中通过接口来实现多态,不同的类型实现对应接口,然后调用接口变量的方法,结果取决于接口存储的对应类型的方法

多态: 指一个类实例的相同方法能有不同表现形式。多态机制可以让不同内部结构的对象共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。

package main

import "fmt"

type Shape interface {
Area() float64
}

type Circle struct {
R float64
}

func (c Circle) Area() float64 {
return 3.14159 * c.R * c.R
}

type Rect struct {
W, H float64
}

func (r Rect) Area() float64 {
return r.W * r.H
}

func printArea(s Shape) {
fmt.Println(s.Area()) // 多态:同一调用,行为取决于具体类型
}

func main() {
printArea(Circle{R: 2}) // 圆
printArea(Rect{W: 3, H: 4}) // 矩形

// 也可以放进同一个切片里统一处理
shapes := []Shape{Circle{2}, Rect{3, 4}}
for _, s := range shapes {
fmt.Println(s.Area())
}
}

3. golang 中 make 和 new 的区别?(基本必问)

分析 考察go语言的基础,对象的创建方式,make和new都可以用来创建对象,但是make创建对象有一定的限制,回顾我们平常写代码的过程,只是针对某些特定的数据类型一般用make来创建,比如slice,map,还有channel,以及创建完之后,返回值是什么?回答的要点要突出创建对象的区别以及返回值类型。

回答

  • make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;

  • new 分配返回的是指针,即类型 *Type。make 返回数据类型本身,即 Type;

  • new 分配的空间被清零。make 分配空间后,会进行初始化;new 只负责“分配 + 置零”,make 负责“创建并初始化某些内置结构”(slice/map/channel)

new:分配 T 的零值并返回 *T(类似 malloc+memset)。 make:仅用于 slice/map/chan,分配并初始化其内部结构,返回可用的 T(类似创建并初始化运行时对象)。

a := new([]int)
fmt.Println(a)

&[]

在这里插入图片描述 在这里插入图片描述

面试常见追问:为什么 new(map) 不如 make(map)?

因为 new(map[string]int) 得到的是 *map[string]int,但里面还是 nil map,写入会 panic;而 make(map[string]int) 直接得到可写 map。

pm := new(map[string]int)
// (*pm)["x"] = 1 // panic: assignment to entry in nil map

m := make(map[string]int)
m["x"] = 1 // ok

4. 数组和切片的区别(基本必问),切片怎么扩容

分析 数组和切片在编程中都会用到,都是用来存储一组相同数据的内存连续的数据结构,主要区别在于长度是否固定且数据类型的性质,是否是引用数据类型,比如在做函数参数传递时,是否会影响到原数据(数组或切片),这就需要了解切片的底层实现,回顾下Go语言切片的底层实现,

切片的底层数据结构定义如下:

type slice struct {
// 底层数组指针(或者说是指向一块连续内存空间的起点)
array unsafe.Pointer
// 长度
len int
// 容量
cap int
}

在这里插入图片描述

slice结构 包含三个字段,array 类型为unsafe.Pointer,还有两个int类型的字段len和cap。

  • array:是一个指针变量,指向一块连续的内存空间,即底层数组结构

  • len:当前切片中数据长度

  • cap:切片的容量

注意:cap是大于等于len的,当cap大于len的时候,说明切片未满,它们之间的元素并不属于当前切片。(len 到 cap-1 那段位置在底层数组里存在,但没被当前切片包含,所以不能直接索引,只有扩展/append 才可能用到。比如说, s:=make([]int, 2,5) 那么这个切片的len就是2,cap是5, 打印s的话,会发现是[0,0],不会有len到cap-1之间的那3个0,所以说len和cap-1之间的元素不属于当前切片)

回答

数组是值类型,长度固定 切片是引用类型,长度不固定,可以动态扩容

在这里插入图片描述 在这里插入图片描述

切片怎么扩容?

分析 这个问题其实是对上个问题的补充提问,因为切片的长度不固定,可以动态扩容,所以需要了解其具体的扩容策略是怎样的,这里回答的要点是需要区分go的版本,在go1.17之前和之后扩容策略是不一样的

回答

1.17及以前 a. 如果期望容量大于当前容量的两倍就会使用期望容量; b. 如果当前切片的容量小于 1024 就会将容量翻倍; c. 如果当前切片的容量大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量; 1.18之后

扩容公式:

newcap += (newcap + 3*threshold) / 4

在这里插入图片描述

为什么代码中测试扩容大小和策略对不上?

https://zhuanlan.zhihu.com/p/612436072

package main

import "fmt"

func main() {
s := make([]int, 0, 1000)
fmt.Println("start:", len(s), cap(s))

// 追加到刚好超过 cap,强制触发扩容
for i := 0; i < 1100; i++ {
s = append(s, i)
if i == 1000 || i == 1001 {
fmt.Println("after append", i, ":", len(s), cap(s))
}
}

fmt.Println("end:", len(s), cap(s))
}

root@GoLang:~/proj/goforjob# go run main.go
start: 0 1000
after append 1000 : 1001 1536
after append 1001 : 1002 1536
end: 1100 1536

在这里插入图片描述

5. for i, v := range 切片,v地址会变化吗

分析 for range是面试中经常会出现的一个问题,用于考察面试者语言基础是否扎实。主要会考察写代码时经常遇到的一些坑,这里就要对for range整个语法糖有一个深层次的了解,for range在遍历的时候,其实它的底层实现是这样的,会对原切片做一次拷贝,确定其值和长度,遍历数组中每个元素的时候都把这个值赋给同一个临时变量,所以每次遍历拿到的是同一个地址,但是值不同

Len_temp := len(range)
range_temp := range
for index_temp = 0; index_temp < len_temp; index_temp++ {
value_temp = range_temp[index_temp]
index = index_temp
value = value_temp
original body
}

Go1.25.4

package main

import "fmt"

func main() {
s := []int{10, 20, 30}
for i, v := range s {
fmt.Printf("i=%d &v=%p v=%d\\n", i, &v, v)
}
}

root@GoLang:~/proj/goforjob# go run main.go
i=0 &v=0xc000012098 v=10
i=1 &v=0xc0000120b8 v=20
i=2 &v=0xc0000120c0 v=30

回答

地址不会发生变化,但是该地址的值是变化的,每遍历到一个元素,就把该元素值就会写到该地址

PS:在最新版本Go 1.22中,v的地址会变化的,也就是不再共享变量了,知道这点的话,在面试一定要提出来,展示自己的技术面广

追加一个思考题

for range 循环遍历 slice 有什么问题?

a. 对slice用for range遍历,遍历过程中追加元素不会遍历到

对 slice 用 for range 遍历时,循环开始就把要遍历的长度固定下来了,所以遍历过程中 append 出来的新元素通常不会被遍历到。

更精确说法:range 开始时会先取 len(s) 作为迭代次数(并拿到当前的 slice 头信息),后面即使你 append 让 len(s) 变大,循环次数也不变

1、for range实际在循环之前就获取到了切片的长度,按照之前的长度来循环的 2、一旦发生动态扩容,会创建一个新的底层数组,并拷贝旧的数组。所以for range里使用的数组,和发生动态扩容的数组,不是一个。 所以不会遍历追加元素

package main

import "fmt"

func main() {
s := []int{1, 2, 3}
for i, v := range s {
fmt.Println(i, v)
if v == 2 {
s = append(s, 4, 5)
}
}
fmt.Println("final:", s)
}

root@GoLang:~/proj/goforjob# go run main.go
0 1
1 2
2 3
final: [1 2 3 4 5]

6. go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?(defer 和 return)

6.1 defer 的执行顺序

分析 关于go语言中的defer,需要明确defer的作用和执行机制,一般用defer来做什么,优势在什么地方,defer在函数返回前执行过程又是怎样的?在回答的时候要突出顺序是LIFO这个特性,接着可以简单介绍下defer底层实现是怎么实现的

可以回顾defer的底层实现: 底层存储如下图: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

defer函数在注册的时候,创建的_defer结构会依次插入到_defer链表的表头,在当前函数return的时候,依次从_defer链表的表头取出_defer结构执行里面的fn函数,所以执行顺序是LIFO。

回答

defer的执行顺序类似于栈,是LIFO,先调用的defer语句后执行。

6.2 defer在什么时机会修改返回值

分析 defer在return的时候有机会修改返回值,函数的返回其实不是一个原子操作,可以理解为三个步骤: a. 设置返回值 b. 执行defer语句 c. 将结果返回

在这里插入图片描述

回答

命名返回值:defer 直接改命名返回变量

func f() (res int) {
res = 1
defer func() { res = 2 }()
return // 返回前 defer 把 res 改成 2
}
// 返回 2

defer recover 的问题?(主要是能不能捕获)

a. 用recover捕获异常时,只能捕获当前goroutine的panic,不能捕获其他goroutine发生的panic b. 一个recover只能捕获一次panic,且一一对应

7. uint 类型溢出

分析 关于整型溢出主要是考察go语言基本数据类型的大小范围是个了解,以及各种数据类型的占用和空间大小,下表整理了go语言基本数据类型的大小范围,溢出主要是关注无符号整数的溢出

在这里插入图片描述

uint8大小为1个字节,占8位,byte其实就是uint8的别名,uint8的溢出情况举例:

var a uint8 = 255
var b uint8 = 1
a+b = 0 // 溢出

8. 介绍 rune 类型

分析 主要考察对go语言基本数据类型有没有细致的了解,rune类型是go语言一种特殊的数据类型,回答的时候重点要突出rune类型具体在底层对应什么数据类型(int32),以及它的作用是用作处理字符的

  • uint8 类型,或者叫 byte 类型,代表了 ASCII 码的一个字符。

  • rune 类型,代表一个 UTF-8 字符,当需要处理中⽂、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

package main
import "fmt"

func main() {
var str = "hello 你好" //思考下 len(str) 的长度是多少?//golang中string底层是通过byte数组实现的,直接求len 实际是在按字节长度计算 //所以一个汉字占3个字节算了3个长度
fmt.Println("len(str):", len(str)) // len(str): 12//通过rune类型处理unicode字符
fmt.Println("rune:", len([]rune(str))) //rune: 8
}

回答

rune类型是 Go 语言的一种特殊数字类型。在builtin/builtin.go文件中,它的定义:type rune = int32; 官方对它的解释是:rune是类型int32的别名,在所有方面都等价于它,用来区分字符值跟整数值。使用单引号定义,返回采用 UTF-8 编码的 Unicode 码点。Go 语言通过rune处理中文,支持国际化多语言。

9. Go中两个Nil可能不相等吗?

分析 两个数据要进行比较,首先得明白数据类型,对于两个nil的比较同样如此,这里主要得注意interface类型,因为interface类型是类型T和值V二者的综合,只有在类型T和值V都相等的情况下,两个interface才会相等

接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。

在这里插入图片描述

两个接口值比较时,会先比较 T,再比较 V。接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。

func main() {
var p *int = nil
var i interface{} = p
fmt.Println(i == p) // true
fmt.Println(p == nil) // true
fmt.Println(i == nil) // false
}

例子中,将一个nil非接口值p赋值给接口i,此时,i的内部字段为(T=*int, V=nil),与p作比较时,将 p 转换为接口后再比较,因此 i == p,p 与 nil 比较,直接比较值,所以 p == nil。

但是当 i 与nil比较时,因为i为接口值,会将nil转换为接口(T=nil, V=nil),与(T=*int, V=nil)不相等,因此 i != nil。因此 V 为 nil,但 T 不为 nil 的接口不等于 nil。

回答​

Go中两个Nil可能不相等,当一个接口类型的变量为nil(类型非空,值为nil)和一个非接口类型的变量也为nil的时候,虽然两者都为nil,但是却不相等。

10. golang 中解析 tag 是怎么实现的?反射原理是什么?(问的很少,但是代码中用的多)

解析 tag

分析 首先要明白,tag 是什么以及 tag 的规则,go 语言中的 tag 就是结构体中的各个字段的一个标签,Tag 本身是一个字符串,它是以空格分隔的 key:value 对,

  • key:必须是非空字符串,不能包含控制字符、空格、引号、冒号

  • value:以双引号标记的字符串

  • 注意:冒号前后不能有空格

举个例子

type Student struct {
Name string `key1:"value1" key2:"value2"` //名字
Age uint `key3:"value3"` //年龄
}

在这里插入图片描述

``之间的就是一个 tag,第一个问题就是要回答怎样获取这个 tag 的值,一般是通过反射来实现

回答

Go 中解析的 tag 是通过反射实现的

反射的实现原理

分析 主要考察反射的实现机制,即获取数据的动态类型和动态值,联想一下我们学习的知识,在哪一节讲过动态类型和动态值,就是 interface,所以这题回答的关键点就是要点出 interface,然后介绍下,interface 的底层实现

回答

反射是指计算机程序在运行时(Runtime)可以访问、检测和修改它本身状态或行为的一种能力 Go 语言反射是通过接口来实现的,通过隐式转换,普通的类型被转换成 interface 类型,这个过程涉及到类型转换的过程,首先从 Golang 类型转为 interface 类型,再从 interface 类型转成反射类型,再从反射类型得到想的类型和值的信息

在这里插入图片描述

推荐学习 https://golang.design/go-questions/stdlib/reflect/how/

11. go struct 能不能比较?

分析 主要考察对 go 语言基本数据类型的掌握程度,其实本质就是考察哪些数据结构不能比较,哪些可以比较,在 go 语言中,回答的时候,要明确可比较的范围,很明显,两个不同类型的数据类型不能进行比较,struct 包含不可比较的字段也是不可比较的。

回答

  • 对于不同类型的 struct 无法进行比较;而同一个 struct 的两个实例可比较也不可比较。
  • 在 Go 中,Slice、map、func 无法比较,当一个 struct 的成员是这三种类型中的任意一个,就无法进行比较。反之,struct 是可以进行比较的
  • chan 是可比较类型,可以用 == / != 比较 两个 chan 相等:它们引用的是同一个底层 channel(同一个通道对象)

    chan == nil:判断通道是否为零值(未初始化)

    var ch1 chan int
    fmt.Println(ch1 == nil) // true

    ch2 := make(chan int)
    ch3 := ch2
    fmt.Println(ch2 == ch3) // true(同一个 channel)

    ch4 := make(chan int)
    fmt.Println(ch2 == ch4) // false(不同 channel)

    package main

    import (
    "fmt"
    "reflect"
    )

    func main() {

    var ch chan int
    if ch == nil {
    fmt.Println("通道为nil")
    }

    ch = make(chan int)
    if ch != nil {
    fmt.Println("通道不为nil")
    }

    ch1 := make(chan int)
    ch2 := ch1
    ch3 := make(chan int)

    fmt.Println(reflect.ValueOf(ch1).Pointer() == reflect.ValueOf(ch2).Pointer())
    fmt.Println(reflect.ValueOf(ch1).Pointer() == reflect.ValueOf(ch3).Pointer())
    }

    reflect.ValueOf(ch).Pointer() 用来拿到 channel 底层对象的地址标识,用它比较就是在判断两个 channel 变量是不是指向 同一个通道。

    在这里插入图片描述

    12. 结构体打印时,%v 和 %+v 的区别

    分析 看下面代码示例

    package main

    import "fmt"

    type student struct {
    id int32
    name string
    }

    func main() {
    a := &student{id: 1, name: "张三"}

    fmt.Printf("a=%v \\n", a) // a=&{1 张三}
    fmt.Printf("a=%+v \\n", a) // a=&{id:1 name:张三}
    fmt.Printf("a=%#v \\n", a) // a=&main.student{id:1, name:"张三"}
    }

    回答

    %v 输出结构体各成员的值; %+v 输出结构体各成员的名称和值; %#v 输出结构体名称和结构体各成员的名称和值

    在这里插入图片描述

    13. 空 struct{} 占用空间么?

    分析 可以使用 unsafe.Sizeof 计算出一个数据类型实例需要占用的字节数:

    package main

    import (
    "fmt"
    "unsafe"
    )

    func main() {
    fmt.Println(unsafe.Sizeof(struct{}{})) //0
    }

    unsafe.Sizeof 返回 uintptr,因为那是“机器层面的字节大小/地址宽度概念”

    package main

    import (
    "fmt"
    "unsafe"
    )

    type T1 struct {
    a struct{}
    b int
    }

    type T2 struct {
    b int
    a struct{}
    }

    func main() {
    fmt.Println(unsafe.Sizeof(T1{}))
    // 8

    fmt.Println(unsafe.Sizeof(T2{}))
    // 16
    }

    root@GoLang:~/proj/goforjob# go run main.go
    8
    16

    如果空struct是别的struct末尾的字段则会占空间

    在这里插入图片描述

    回答

    空结构体 struct{} 实例不占用任何的内存空间。

    14. go语言中空 struct{} 的用途?

    分析 这个题目可以看做是上一题的补充提问,通过上一题我们知道空结构体 struct{} 不占用任何内存,那么这题就可以转化思考,不占用内存的 struct 有什么用处?正因为不占用内存,所以空 struct 被广泛作为各种场景下的占位符使用

    1. 将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。

    package main

    import (
    "fmt"
    )

    type Set map[string]struct{}

    func (s Set) Has(key string) bool {
    _, ok := s[key]
    return ok
    }

    func (s Set) Add(key string) {
    s[key] = struct{}{}
    }

    func (s Set) Delete(key string) {
    delete(s, key)
    }

    func main() {
    s := make(Set)
    s.Add("Tom")
    s.Add("Sam")
    fmt.Println(s.Has("Tom"))
    fmt.Println(s.Has("Jack"))
    }

    root@GoLang:~/proj/goforjob# go run main.go
    true
    false

    2. 不发送数据的信道(channel)

    使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。

    package main

    import (
    "fmt"
    )

    func worker(ch chan struct{}) {
    <-ch
    fmt.Println("do something")
    close(ch)
    }

    func main() {
    ch := make(chan struct{})
    go worker(ch)
    ch <- struct{}{}
    }

    root@GoLang:~/proj/goforjob# go run main.go
    do something

    3. 结构体只包含方法,不包含任何的字段

    type Door struct{}

    func (d Door) Open() {
    fmt.Println("Open the door")
    }

    func (d Door) Close() {
    fmt.Println("Close the door")
    }

    回答

  • 将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。

  • 使用在不发送数据的信道(channel)上,使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。

  • 用作接口的实现,结构体只包含方法,不包含任何的字段(例如emptyCtx)

  • 在这里插入图片描述

    15. go中"_"的作用

    分析

    _在go语言中可以出现在不同的位置,可以在import中,也可以在代码中出现,在不同的场合其作用是不同样的,在回答的时候要凸显出在不同场景下,回答全面

  • import中的下滑线 此时"_"的作用是:当导入一个包的时候,不需要把所有的包都导进来,只需要执行使用该包下的文件里所有的init()的函数。
  • package main

    import _ "hello/imp"

    func main() {
    //imp.Print() //编译报错,说:undefined: imp
    }

    或者导入数据库驱动。 很多数据库驱动包(比如 MySQL 驱动)在 init() 里把自己注册到 database/sql 的驱动表里。你业务代码并不会直接调用驱动包的函数,所以就用 _ 只为了执行它的 init()

    在这里插入图片描述 在这里插入图片描述

  • 下划线在代码中
  • 作用是:下划线在代码中是忽略这个变量

    也可以理解为占位符,那个位置上本应该赋某个值,但是我们不需要这个值,所以就把该值给下划线,意思是丢掉不要,这样编译器可以更好的优化,任何类型的单个值都可以丢给下划线。

    如果方法返回两个值,只想要其中的一个结果,那另一个就用_占位

    package main

    import "fmt"

    v1, v2, _ := function(…)

    回答

  • import中的下滑线用于执行导入包下的所有init函数

  • 代码体中的下划线用于忽略返回值

  • 16. Go 闭包

    分析

    主要考察对go语言对匿名函数的支持,回答的时候点出匿名函数关键字即可,面试种不常见

    回答

    闭包实际上就是匿名函数 + 引用环境(捕获的变量)

    a. 在《深度探索Go语言》一书中提到,从语义角度来讲,闭包捕获变量并不是要复制一份副本,变量无论被捕获与否都应该是唯一的,所谓捕获只是编译器为闭包函数访问外部环境中的变量搭建了一个桥梁。这个桥梁可以复制变量的值,也可以存储变量的地址。只有在变量的值不会再改变的前提下,才可以复制变量的值,否则就会出现不一致错误

    在这里插入图片描述

    推荐学习 go语言闭包函数 https://aijishu.com/a/106000000015474

    在这里插入图片描述

    17. Go 多返回值怎么实现的?

    分析 主要考察go语言中对函数栈帧的理解程度,要清楚go语言中函数调用过程中,函数栈帧是怎样保存各个寄存器值的,以及栈帧的布局是怎样的,回答这个问题要突出go语言函数调用是通过fp寄存器+offset来实现的

    回顾一下函数栈帧布局 在这里插入图片描述

    回答

    Go函数传参是通过fp+offset来实现的,而多个返回值也是通过fp+offset存储在调用函数的栈帧中

    FP(‌Frame Pointer)‌:‌FP是一个伪寄存器,‌它对应于函数的栈帧底部。‌在Golang中,‌函数的参数和返回值、‌函数中的局部变量以及函数中调用子函数的参数和返回值都存储在栈中,‌这一段栈内存被称为栈帧(frame)。‌伪FP寄存器用于访问函数的参数和返回值。‌尽管伪FP寄存器只包括函数的参数和返回值这部分内存,‌但栈帧的其他部分由伪SP寄存器表示。‌需要注意的是,‌Golang中的函数返回值也是通过栈帧返回的,‌这是Golang函数可以有多个返回值的原因之一。‌

    推荐学习 https://www.w3cschool.cn/go_internals/go_internals-vn2d282m.html

    Go 的 Plan9 汇编语法里常用 FP/SP + offset 来访问参数/局部变量,但底层调用约定和 C 一样,本质是寄存器优先、必要时落栈的混合方案;不是 Go 用栈而 C 用寄存器。

    SP:常用来找局部变量(函数自己用的)

    FP:常用来找参数/返回值(函数对外接口的那块)

    SP栈指针

    SP 是 Stack Pointer 的缩写,中文一般叫 栈指针。 在这里插入图片描述

    FP帧指针

    FP 是 Frame Pointer 的缩写,中文常叫 帧指针(也有人叫“栈帧指针”)。 在 Go 的 Plan9 风格汇编里,FP 多数时候是一个伪寄存器,用来作为基址按偏移访问函数的参数和返回值槽位(比如 a+0(FP)、ret+8(FP) 这种)。

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述 在这里插入图片描述

    https://go.cyub.vip/function/call-stack.html

    在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

    18. Go 语言中不能比较的类型如何比较是否相等?

    分析 这个题目主要考察对reflect.DeepEqua的了解,因为基本类型都可以用==来比较,但是涉及到像不能比较的类型,比如slice,map怎么比较,所以再回答的时候要点出关键字reflect.DeepEqual

    回答

  • 像 string, int, float, interface 等可以通过 reflect.DeepEqual 和等于号进行比较

  • 像 slice, struct, map 则一般使用 reflect.DeepEqual 来检测是否相等。

  • 在这里插入图片描述

    推荐学习 https://golang.design/go-questions/stdlib/reflect/compare/

    https://juejin.cn/post/7170332084144177165

    19. Go 中 init 函数的特征?

    分析 主要考察go语言的初始化过程,初始化过程中分为全局变量,init函数,在初始化过程中主要是要明确他们的初始化顺序,所以回答要点要突出各个包下的全局变量,init函数,它们的执行顺序

    在这里插入图片描述

    回答

  • 每个包下可以有多个 init 函数,每个文件也可以有多个 init 函数。多个 init 函数按照它们的文件名顺序逐个初始化。

  • 应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到 main 包。 a. 不管包被导入多少次,包内的 init 函数只会执行一次。 b. 而且包级别变量的初始化先于包内 init 函数的执行。

  • 执行顺序:import -> const -> var -> init() -> main()

    20. Go 中 uintptr 和 unsafe.Pointer 的区别?

    分析 考察对go语言中指针的理解,go语言中指针分为普通指针类型,unsafe.Pointer,uintptr(本质不是指针,下面会进行说明)。三者的功能各不相同

    • *类型:普通指针类型,用于传递对象地址,不能进行指针运算。

    • unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(必须转换到某一类型的普通指针)。

    • uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被回收。

    在回答unsafe.Pointer和uintptr的区别时,重点突出指针运算上,unsafe.Pointer用于指针类型转换,不能参与运算,而uintptr可以运算。

    回答

  • unsafe.Pointer 是通用指针类型,它不能参与计算,任何类型的指针都可以转化成unsafe.Pointer,unsafe.Pointer 可以转化成任何类型的指针 a. 当我们想让普通指针类型之间进行转换的时候,就需要unsafe.Pointer作为中间指针
  • var b *float64
    var a int = 100
    var c *int = &a
    b = (*float64)(unsafe.Pointer(c))

  • uintptr 可以转换为 unsafe.Pointer,unsafe.Pointer 可以转换为 uintptr。uintptr 是指针运算的工具,但是它不能持有指针对象(意思就是它跟指针对象不能相互转换),unsafe.Pointer 是指针对象进行运算(也就是uintptr)的桥梁。 a. 很多人都认为uintptr是个指针,其实不然。不要对这个名字感到疑惑,它只不过是个uint,大小与当前平台的指针宽度一致。因为unsafe.Pointer可以跟uintptr互相转换,所以Go语言中可以把指针转换为uintptr进行数值运算,然后转换回原类型,以此来模拟C语言中的指针运算。 b. unsafe.Pointer类似于C语言中的void*,虽然未指定元素类型,但是本身类型就是个指针。
  • package main

    import (
    "fmt"
    "unsafe"
    )

    func main() {
    arr := [4]int64{10, 20, 30, 40}

    base := unsafe.Pointer(&arr[0])
    i := 2

    // base + i*sizeof(int64)
    pi := (*int64)(unsafe.Pointer(uintptr(base) + uintptr(i)*unsafe.Sizeof(arr[0])))

    fmt.Println(*pi) // 30
    }

    推荐学习 https://www.cnblogs.com/-wenli/p/12682477.html

    之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

    赞(0)
    未经允许不得转载:171主机测评 » Golang面试题库(基础相关)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址