大厂面试题
For-Range
1. 以下代码是死循环么?
1 | func main() { |
答: 不会死循环, 只会循环3次
对于切片的for range,它的底层代码就是:
1 | // for_temp := range |
从上述代码可以看出第2行, 在进入循环之前已经确定好循环次数, 所以这段代码不会死循环。
2.下面这段代码有什么问题?
1 | slice := []int{0, 1, 2, 3} |
输出的结果 value 一样
1 | == ===new map===== |
循环切片,index和value地址一开始分配好后,后面还是那个地址。结果完全一样,都是最后一次遍历的值。通过上面的底层代码看下,遍历后的值赋给了value,而在我们的例子中,会把value的地址保存到myMap的值中。这里的value是个「全局变量」,所以赋完值之后myMap里面所有的值都是value,所以结构都是一样的而且是最后一个值。
注意: 这里必须是保存指针才会有问题,如果直接保存的是value,因为 Golang 是值拷贝,所以值会重新复制再保存,这种情况下结果就会是正确的了。
3. var i interface{} = 3 会额外分配堆内存么?
1 | var a int = 3 |
不需要进行内存分配
接口类型
首先,在Go中,接口类型可以简单表示为:
1 | type iface struct { |
其中 tab
是指向类型信息的指针; data
是指向值的指针。因此,一般来说接口意味着必须再堆中动态分配。
然而,从Go 1.15 开始,在 runtime
部分提到了一个改进:
Converting a small integer value into an interface value no longer causes allocation.
将小整数转换为接口值不再需要进行内存分配。(小整数是指 0 到 255 之间的数)
convert a small integer
Benchmark:
1 | package integer |
integer_test.go
1 | package integer_test |
分别使用 Go1.14 和 Go1.15 版本进行测试:
1 | go version |
1 | go version |
从上述测试结果可以清晰地看出,Go1.15版本执行分配了0次内存,而Go1.14.15分配了102次内存。我们将Convert
中的参数由50改为500,再次运行:
1 | go test -bench . -benchmem ./... |
实际上,Go 以前有一个优化,如果你将 0 转换为接口值,它将返回一个指向特殊静态零值的指针。这次新的 0-255 优化替代了该值。
对具体实现细节感兴趣的,可以阅读提交。
4. 以下代码输出什么? 如果去掉注释又会输出什么?
1 | package main |
equal, invalid operation: a[:] == b[:] (slice can only be compared to nil)
切片之间不支持逻辑运算符,仅能判断是否为nil,比如:
1 | var a []int |
5. 请问 n 是多少?
1 | var m int32 = 0x12345678 |
结果: C
为什么呢?
我们加一下代码来看看
1 | var m int32 = 0x12345678 |
打印结果:
1 | 00010010001101000101011001111000 |
可见,转换为int8后,只取了后面8bit
如果转为int16呢?
再添加代码
1 | var m int32 = 0x12345678 |
打印结果:
1 | 00010010001101000101011001111000 |
6. 以下代码是否正确? 为什么?
1 | package main |
不正确, 无法通过
golang中不能寻址的可以总结为:不可变的,临时结果和不安全的。只要符合其中任何一个条件,它就是不可以寻址的。
- 常量的值
- 基本类型值的字面量
- 算术操作的结果值
- 对各种字面量的索引表达式和切片表达式的结果值
- 不过有一个例外,对切片字面量的索引结果值却是可寻址的
- 对字符串变量的索引表达式和切片表达式的结果值
- 对字典变量的索引表达式的结果值
- 函数字面量和方法字面量,以及对它们的调用表达式的结果值
- 结构体字面量的字段值,也就是对结构体字面量的选择表达式的结果值
- 类型转换表达式的结果值
- 类型断言表达式的结果值
- 接收表达式的结果值
7. 以下代码输出什么? 为什么?
1 | package main |
32 0
本题设计几个知识点:
len 函数
len 是一个内置函数。在官方标准库文档关于 len 函数有这么一句:
For some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification’s “Length and capacity” section for details.
当参数是字符串字面量和简单 array 表达式,len 函数返回值是常量。
如果将上述代码中 const s = "abnerwei.com"
改为 var s = "abnerwei.com"
, 结果是否有变化?
1 | package main |
接着, 我们再将变量s
改为[]byte
1 | package main |
内置函数
len
和cap
获取各种类型的实参并返回一个int
类型结果。实现会保证结果总是一个int
值。
如果s
是一个字符串常量,那么len(s)
是一个常量 。如果s
类型是一个数组或到数组的指针且表达式s
不包含 信道接收 或(非常量的) 函数调用的话, 那么表达式len(s)
和cap(s)
是常量;这种情况下,s
是不求值的。否则的话,len
和cap
的调用结果不是常量且s
会被求值。
1 | var a byte = 1 << len(s) / 128 // 第一句的 len(s) 是常量(因为 s 是字符串常量), 值为12 |
位移操作
关于位移操作符描述:
The right operand in a shift expression must have integer type or be an untyped constant representable by a value of type uint. If the left operand of a non-constant shift expression is an untyped constant, it is first implicitly converted to the type it would assume if the shift expression were replaced by its left operand alone.
在位移表达式的右侧的操作数必须为整数类型,或者可以被 uint 类型的值所表示的无类型的常量。如果一个非常量位移表达式的左侧的操作数是一个无类型常量,那么它会先被隐式地转换为假如位移表达式被其左侧操作数单独替换后的类型。
因此对于 var a byte = 1 << len(s) / 128
,因为 1 << len(s)
是一个常量位移表达式,因此它的结果也是一个整数常量,所以是 4096
,最后除以 128
,最终结果就是 32
。
而对于 var b byte = 1 << len(s[:]) / 128
,因为 1 << len(s[:])
不是一个常量位移表达式,而做操作数是 1
,一个无类型常量,根据规范定义它是 byte 类型(根据:如果一个非常量位移表达式的左侧的操作数是一个无类号常量,那么它会先被隐式地转换为假如位移表达式被其左侧操作数单独替换后的类型)。
常量
常量是在编译的时候就确定好的。在 Go 中,字面值常量, true , false , iota 以及一些仅包含无类型的恒定操作数的 常量表达式
是无类型的。
所以 var b byte = 1 << len(s[:]) / 128
中,根据定义,1 会隐式转换为 byte 类型,因此 1 << len(s[:])
的结果也是 byte
类型,而 byte
类型最大只能表示 255
,很显然 4096
溢出了,结果为 0
,因此最后 b 的结果也是 0
。
8. 这道题 reslice题, 结果是什么?
1 | package main |
看到这个题, 你的第一反应是啥?
1 | (A) 编译失败 |
第一感觉len(a1) = 1, a1[1] 肯定会 panic, 但结果为`C: []`
a1, a2 共享共同的底层数组, len(a1) = 1
, a1[1]
绝对会 panic, 但是 a[1:]
却可以正常输出?
调试代码
1 | a1 := []int{3} |
结果:
1 | len: 1, cap: 1 |
从代码来看,从a[2:]才开始 panic,接下来看下汇编代码
汇编代码
1 | "".a STEXT size=87 args=0x18 locals=0x18 |
主要看这两行
1 | 0x002d 00045 (main.go:4) MOVQ 8(SP), AX |
在翻阅了官方文档,了解到这是故意的, 保持 reslice 的对称性。
思考一下:
- a1, a2 是如何共享底层数组的?
- a1[low:high]是如何实现的?
拓展
语法糖代码
map
1 | // Lower a for range over a map. |
channel
1 | // Lower a for range over a channel. |
array
1 | // Lower a for range over an array. |
string
1 | // Lower a for range over a string. |