第 2 篇:Go 基础语法避坑指南:变量、指针与控制流

面试官提问:

"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 中,处理字符串时经常会遇到 runebyte

  • byteuint8 的别名,表示一个字节。
  • runeint32 的别名,表示一个 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 基础类型

对于 intstring 等基础类型,直接用 == 比较即可。

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 不可比较

一针见血的总结:

  • 可比较类型:intstringboolchannelarraystruct(如果字段都可比较)。
  • 不可比较类型:slicemapfunc
  • 接口比较:如果底层类型是不可比较的,运行时会 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 个最容易翻车的点:

  1. uint 溢出:尽量用 int,警惕循环减法。
  2. rune:本质是 int32,处理中文必用。
  3. 比较:Slice/Map 不可比较,接口比较要小心 Panic。
  4. 循环:Go 只有 for,没有 while
  5. 指针uintptr 是数,unsafe.Pointer 是针,GC 对待它们态度不同。

掌握了这些,你已经在基础层面领先了 80% 的 "Hello World" 选手。

下一篇,我们将深入 Go 的核心关键字,聊聊 makenew 的底层区别,以及那个让无数人头晕的 defer 执行顺序

👉 关注专栏,每天进步一点点!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容