整理一些golang面试常见八股,大多来自互联网结合一些个人理解,尽量简单描述,适合面试回答,只建议作为学习补充,不建议开始学习使用,有问题欢迎指正。
官方文档中写的是 Yes and No。也就是说,go并不是面向对象的语言,但是可以进行面向对象风格的编程,具体而言,是通过下面的方法实现的面向对象的三大特性。
为什么说是引用语义,而不是引用类型?
因为go中没有引用类型,全部都是值传递。slice只是看起来是引用,因为它的底层结构体中,是一个指向数组的指针,在进行传递时,依然是传递整个结构体,只是指针指向的地址是相同的。
在1.22版本之前,v的地址不会发生变化,但是该地址的值是变化的,每遍历到一个元素,就把该元素值就会写到该地址,这导致如果在循环中使用v创建协程就会很容易有问题。 在1.22版本,v的地址会发生变化,不再共享变量,也就不会有上面的问题。
defer的执行顺序类似于栈,是先调用后执行。 defer在return的时候有机会修改返回值,return的过程可以被分解为三步:
rune类型是 Go 语言的一种特殊数字类型。它的定义: type rune= int32
;官方对它的解释是
不占用任何空间。用途:
闭包实际上就是匿名函数 +引用环境(捕获的变量)
在《深度探索Go语言》一书中提到,从语义角度来讲,闭包捕获变量并不是要复制一个副本,变量无论被捕获与否都应该是唯一的所谓捕获只是编译器为闭包函数访问外部环境中的变量搭建了一个桥梁。这个桥梁可以复制变量的值,也可以存储变量的地址。只有在变量的值不会再改变的前提下,才可以复制变量的值,否则就会出现不一致错误
有点绕,下面是GPT的解释:
Go函数传参是通过fp+offset来实现的,而多个返回值也是通过fp+offset存储在调用函数的栈帧中 可以看下这个Go语言圣经的详细介绍https://gopl-zh.codeyu.com/ch5/ch5-03.html
执行顺序import -> const -> var ->init()->main()
1.17及以前
1.18之后,阈值从1024变为256
golangDeadline() (deadline time.Time, ok bool) // 第一个返回值代表还有多久超时;第二个返回值表示是否有超时时间控制
Done() <- chan struct{} // 返回一个只读channel,当这个channel被关闭时,说明这个context被取消
Err() error // 返回一个错误,表示该context被关闭的原因,例如被取消、超时关闭
Value(key interface{}) interface{} // 返回指定key对应的value
channel配合goroutine可以用来实现并发编程,并且是go语言推荐的并发编程模式,那么肯定是可以保证线程安全的。 对channel就只有读,写,关闭三种操作,这三种操作,channel底层数据结构都用同一把runtime.Mutex来进行保扩
操作 \ Channel状态 | 未初始化 (nil ) | 关闭 (closed ) | 正常 |
---|---|---|---|
关闭 | panic | panic | 正常关闭 |
写 | 阻塞当前 goroutine,可能永久挂起(死锁) | panic | 正常写入或阻塞 |
读 | 阻塞当前 goroutine,可能永久挂起(死锁) | 读缓冲区数据,读完返回零值 | 正常读取或阻塞 |
map 不是线程安全的。
如果某个任务正在对map进行写操作,那么其他任务就不能对该 字典执行并发操作(读、写、删除),否则会导致进程崩溃。
在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志等于1,则直接 fatal 退出程序。赋值和删除函数在检测完写标志是0之后,先将写标志改成1,才会进行之后的操作。
无序的。先随机选择一个桶,再随机一个槽位开始便利。
因为map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的key,搬迁后,有些 key 的位置就会发生改变。而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。搬迁后,key 的位置发生了重大的变化,这样,遍历 map 的结果就不可能按原来的顺序了。所以,go语言,强制每次遍历都随机开始。
不会释放,删除一个key,可以认为是标记删除,只是修改key对应内存位置的值为空,并不会释放内存,只有在置空这个map的时候,整个map的空间才会被垃圾回后释放
对map进行加读写锁或者是使用sync.map 和原始map+RWLock的实现并发的方式相比,减少了加锁对性能的影响它做了一些优化:可以无锁访问read map,而且会优先操作read map,倘若只操作read map就可以满足要求,那就不用去加锁操作writemap(dirty),所以在某些特定场景中它发生锁竞争的频率会远远小于map+RWLock的实现方式
sync.Map的优缺点:
未初始化的map:
已经初始化,没有任何元素的map为空map,对空map增删改查不会报错
golang type hmap struct {
count int // 元素的个数
B uint8 // buckets 数组的长度就是 2^B 个
overflow uint16 // 溢出桶的数量
buckets unsafe.Pointer // 2^B个桶对应的数组指针
oldbuckets unsafe.Pointer // 发生扩容时,记录扩容前的buckets数组指针
extra *mapextra //用于保存溢出桶的地址
}
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}
type bmap struct {
tophash [bucketCnt]uint8
}
//在编译期间会产生新的结构体
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
Map的底层实现数据结构实际上是一个哈希表。在运行时表现为个指向hmap结构的指针,hmap中有记录了桶数组指针buckets,溢出桶指针以及元素个数等字段。每个桶是一个bmap的数据结构,可以存储8个键值对和8个tophash以及指向下一个溢出桶的指针overflow。为了内存紧凑,采用的是先存8个key过后再存value.
扩容时机:
向 map 插入新 key 的时候,会进行条件检测,符合下面这2个条件就会触发扩容。
扩容机制
扩容方式:
扩容过程并不是一次性进行的,而是采用的渐进式扩容,在插入修改删除key的时候,都会尝试进行搬迁桶的工作,每次都会检查oldbucket是否nil,如果不是nil则每次搬迁2个桶,蚂蚁搬家一样渐进式扩容
从上面的流程可以看出,在判断 hash 冲突,即该位置是否已有其他 key时,肯定是要进行比较的,所以 key 必须得是可比较类型的。像 slice、map、function 就不能作为 key。
Go的interface底层有两种数据结构:eface和iface。
eface是空interface{}的实现,只包含两个指针:-type 指向类型信息data 指向实际数据。这就是为什么空接口能存储任意类型值的原因,通过类型指针来标识具体类型,通过数据指针来访问实际值。
iface是带方法的interface实现,包含 itab 和 data 两部分。 itab 是核心,它存储了接口类型、具体类型,以及方法表。方法表是个函数指针数组,保存了该类型实现的所有接口方法的地址。
iface和eface的核心区别在于是否包含方法信息
类型转换、类型断言 本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。对于类型转换而言,类型转换是在编译期确定的强制转换,转换前后的两个类型要相互兼容才行,语法是 T(value)
。而类型断言是运行期的动态检查,专门用于从接口类型中提取具体类型,语法是 value.(T)
。
安全性差别很大:类型转换在编译期保证安全性,而类型断言可能在运行时失败。所以实际开发中更常用安全版本的类型断言 value, ok := x.(string)
,通过 ok
判断是否成功。
使用场景不同:类型转换主要解决数值类型、字符串、切片等之间的转换问题;类型断言主要用于接口编程,当你拿到一个 interface{}
需要还原成具体类型时使用。
底层实现也不同:类型转换通常是简单的内存重新解释或者数据格式调整;类型断言需要检查接口的底层类型信息,涉及到 runtime
的类型系统。
依赖注入和解耦。通过定义接口抽象,让高层模块不依赖具体实现,比如定义一个 UserRepo
接口,具体实现可以是 MySQL、Redis 或者 Mock 实现。这样代码更容易测试和维护,也符合 SOLID 原则。
多态实现。比如定义一个 Shape
接口包含 Area()
方法,不同的图形结构体实现这个接口,就能用统一的方式处理各种图形。这让代码更加灵活和可扩展。
标准库中大量使用 interface 来提供统一 API。像 io.Reader
、io.Writer
让文件、网络连接、字符串等都能用统一的方式操作;sort.Interface
让任意类型都能使用标准库的排序算法。
还有类型断言和反射的配合使用,比如 JSON 解析、ORM 映射等场景,先用 interface{}
接收任意类型,再通过类型断言或反射处理具体逻辑。
插件化架构也 heavily 依赖 interface。比如 Web 框架的中间件、数据库驱动、日志组件等,都通过接口定义规范,让第三方能够轻松扩展功能。
总之,一般是不推荐比较两个接口值的。
GMP模型是go语言中协程的调度模型
协程在刚创建的时候,会优先加到当前p的本地队列中,等待被调度,当这个p队列满了的时候,本地队列满了时,会将本地队列的一半G和新创建的 G一起放入全局队列。每个m都有一个特殊的协程g0负责调度工作每一轮调度过程是这样的,M 优先执行其所绑定的P的本地运行队列中的 G,如果本地队列没有 G,则会从全局队列获取,为了提高效率和负载均衡,会从全局队列获取多个 G,而不是只取一个,同样,当全局队列没有时,会从其他 M 的P上偷取 G 来运行,偷取的个数通常是其他P运行队列的一半;如果还没有获取到g,则m就处于自旋状态。
本文作者:AstralDex
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!