面试官提问:
"uint 类型在什么情况下会溢出?"
"Go 语言中,两个 interface{} 类型的变量如何比较是否相等?"
"Go 里面有 while 循环吗?如果没有,怎么实现?"
"函数返回局部变量的指针安全吗?uintptr 和 unsafe.Pointer 有什么区别?"
大家好,我是公号「左诗右码」作者 Alex。
上一篇我们聊了 Go 的宏观优势,今天我们把视角拉近,聚焦在 Go 的基础语法上。
你可能会觉得:基础语法有什么好讲的?不就是定义变量、写写循环吗?
错!Go 的设计哲学虽然崇尚“简单”,但在看似简单的语法背后,藏着不少“反直觉”的坑。这些坑不仅是新手容易犯错的地方,更是面试官用来筛选你是否真的写过 Go 代码的试金石。
今天,我们将通过 5 个高频面试题,带你避开这些语法陷阱。
1. uint 类型溢出问题(隐形炸弹)
在 Go 中,uint 是无符号整型。很多同学在写循环或者做减法时,喜欢随手用 uint,结果埋下了巨大的隐患。
1.1 案发现场
package main
import (
"fmt"
)
func main() {
var a uint = 0
var b uint = 1
// 预期:-1,实际:18446744073709551615 (在 64 位机器上)
fmt.Println(a - b)
}
为什么会这样?
因为 uint 是无符号的,它的值永远大于等于 0。当 0 - 1 时,它不会变成 -1,而是会发生下溢(Underflow),变成该类型的最大值(MaxUint64)。
1.2 循环中的死循环陷阱
// 这是一个死循环!
for i := uint(10); i >= 0; i-- {
fmt.Println(i)
}
当 i 变成 0 后,再执行 i--,i 会变成最大正整数,条件 i >= 0 永远成立。
一针见血的避坑指南:
- 除非你有极强的理由(如位运算、协议解析),否则优先使用
int,哪怕数值不可能是负数。- Go 语言中
len()返回的是int,而不是uint,就是为了防止这种减法溢出。
2. rune 类型是什么?(字符的本质)
在 Go 中,处理字符串时经常会遇到 rune 和 byte。
-
byte:
uint8的别名,表示一个字节。 -
rune:
int32的别名,表示一个 Unicode 码点。
2.1 为什么要区分?
Go 的字符串底层是 UTF-8 编码的字节数组。对于 ASCII 字符(如 'a'),占用 1 个字节;但对于中文字符(如 '中'),通常占用 3 个字节。
package main
import "fmt"
func main() {
str := "Go语言"
// len() 返回的是字节数
fmt.Println(len(str)) // 输出:8 (2个英文字母 + 2个汉字*3)
// 想要获取字符数,需要转成 rune 切片
fmt.Println(len([]rune(str))) // 输出:4
}
面试满分回答:
rune本质上是int32,用于表示一个 Unicode 字符。当我们需要遍历包含中文的字符串,或者计算字符数量时,必须将字符串转换为[]rune,否则可能会出现乱码或截断。
3. Go 如何比较两个变量是否相等?(接口比较的坑)
这是一个经典的坑题。
3.1 基础类型
对于 int、string 等基础类型,直接用 == 比较即可。
3.2 Slice 和 Map(不可比较)
Go 语言规定:Slice 和 Map 只能与 nil 比较,不能相互比较。
s1 := []int{1, 2}
s2 := []int{1, 2}
// fmt.Println(s1 == s2) // 编译报错:slice can only be compared to nil
如果你非要比较,可以使用 reflect.DeepEqual(性能较差)或 Go 1.21 引入的 slices.Equal / maps.Equal。
3.3 Interface{} 的比较(重点)
接口类型的变量,只有在 动态类型 和 动态值 都相等时,才相等。
var a interface{} = []int{1}
var b interface{} = []int{1}
// fmt.Println(a == b) // 运行时 Panic!因为底层类型 slice 不可比较
一针见血的总结:
- 可比较类型:
int、string、bool、channel、array、struct(如果字段都可比较)。- 不可比较类型:
slice、map、func。- 接口比较:如果底层类型是不可比较的,运行时会 Panic。
4. Go 有 while 循环吗?(极简主义)
Go 语言没有 while 关键字,也没有 do-while。Go 只有 for 关键字,但 for 可以通过不同写法实现 while 的功能。
4.1 实现 while
// 相当于 while (i < 10)
i := 0
for i < 10 {
fmt.Println(i)
i++
}
4.2 实现 while(true)
// 相当于 while (true)
for {
// 业务逻辑
}
面试满分回答:
Go 只有
for关键字。这是 Go 极简主义的体现——"一种事情只有一种做法"。通过省略for的初始化语句和后置语句,就可以完美替代while。
5. uintptr 和 unsafe.Pointer 的区别?(黑魔法)
这是 Go 进阶面试中的常客,涉及 Go 的内存管理和 GC。
5.1 概念区分
-
unsafe.Pointer:通用的指针类型,类似于 C 语言的
void*。- 它可以包含任意类型的地址。
- GC 能够识别它指向的对象,不会回收该对象。
-
uintptr:一个足够大的整数类型(unsigned integer),用于存储指针的数值。
- 它仅仅是一个数值,GC 不会把它当做指针。
- 如果一个对象只有
uintptr指向它,GC 可能会回收该对象。
5.2 使用场景
通常用于指针运算(Go 的普通指针不支持运算)。
错误写法(危险):
p := unsafe.Pointer(&x)
u := uintptr(p)
// ... 这里发生了 GC ...
p = unsafe.Pointer(u + offset) // 此时 x 可能已经被回收了!
正确写法(原子操作):
// 必须在一条语句中完成转换,防止中间发生 GC
p := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + offset)
一针见血的总结:
unsafe.Pointer是指针,能防止对象被回收,用于不同指针类型转换。uintptr是整数,用于指针运算,不能防止对象被回收。- 两者转换时必须格外小心,避免中间产生 GC 导致野指针。
🏆 总结与思考
今天我们聊了 Go 基础语法中 5 个最容易翻车的点:
-
uint 溢出:尽量用
int,警惕循环减法。 -
rune:本质是
int32,处理中文必用。 - 比较:Slice/Map 不可比较,接口比较要小心 Panic。
-
循环:Go 只有
for,没有while。 -
指针:
uintptr是数,unsafe.Pointer是针,GC 对待它们态度不同。
掌握了这些,你已经在基础层面领先了 80% 的 "Hello World" 选手。
下一篇,我们将深入 Go 的核心关键字,聊聊 make 和 new 的底层区别,以及那个让无数人头晕的 defer 执行顺序。
👉 关注专栏,每天进步一点点!