Go语言基础语法

1. 使用值为 nil 的 slice、map 会发生什么?

  • Slice:允许对值为 nil 的 slice 添加元素(append),不会报错,Go 会自动初始化。
  • Map:禁止对值为 nil 的 map 添加元素,会造成运行时 panic。必须使用 make 初始化后才能写入。

2. 访问 map 中的 key,需要注意什么?

当访问 map 中不存在的 key 时,Go 会返回该类型的零值(如 0, “”, nil),而不是报错。因此,不能仅通过返回值是否为零值来判断 key 是否存在。
正确姿势:使用 value, ok := map[key],通过 ok(bool)来判断 key 是否存在。

3. string 类型的值可以修改吗?

不能。String 是只读的 byte slice。尝试通过索引(如 s[0] = 'a')修改会报错。
解决方法:将 string 转为 []byte 或 []rune,修改后再转回 string。

4. switch 中如何强制执行下一个 case 代码块?

Switch 默认自带 break。如果需要执行下一个 case,需显式使用 fallthrough 关键字。

5. 你是如何关闭 HTTP 的响应体的?

必须在处理 HTTP 请求的错误检查之后,手动调用 defer resp.Body.Close()。如果不关闭,可能会导致 socket 文件描述符泄漏。

6. 你是否主动关闭过 http 连接,为啥要这样做?

HTTP 标准库默认启用长连接(Keep-Alive)。如果不需要复用连接(如大量一次性请求),应主动关闭,避免耗尽 socket。
方法:

  1. 设置 req.Close = true。
  2. 设置 Header Connection: close。

7. 解析 JSON 数据时,默认将数值当做哪种类型?

json.Unmarshal 解析到 interface{} 时,默认将数值按照 float64 处理。如果需要 int,需要手动转换。

8. 如何从 panic 中恢复?

在 defer 延迟执行的函数中调用 recover()。它能捕捉并中断 panic,防止程序崩溃。

9. 简短声明的变量(:=)需要注意什么?

  1. 只能在函数内部使用。
  2. 左侧至少要有一个新变量。
  3. 不能用于 struct 的字段赋值。

10. range 迭代 map 是有序的吗?

无序的。Go 运行时为了防止开发者依赖遍历顺序,特意引入了随机化。

11. recover 的执行时机?

必须在 defer 函数中直接调用 recover() 才有效。

  • 不能在 defer 嵌套函数中调用。
  • 不能在主函数直接调用。

12. 闭包错误引用同一个变量问题怎么处理?

在 Go 1.22 之前,for 循环中的变量是复用的。如果在闭包(goroutine)中使用该变量,所有闭包可能拿到同一个值。
解决:在循环体内用新变量承接:v := v,或将变量作为参数传递给闭包。

13. 在循环内部执行 defer 语句会发生什么?

defer 是在函数退出时才执行,而不是循环结束时。如果在循环中(如处理文件)使用 defer,会导致资源一直无法释放,直到函数结束,可能导致内存泄漏或句柄耗尽。

14. 说出一个避免 Goroutine 泄露的措施

使用 context 包(ctx, cancel := context.WithCancel)。通过 select 监听 ctx.Done() 信号,在主程退出或超时的时候,通知子协程退出。

15. 如何跳出 for select 循环?

在 select 中使用 break 只能跳出 select,无法跳出外层 for。
解决:使用 break label(标签)或 return。

16. 如何在切片中查找?

如果切片是有序的,可使用 sort.Search(二分查找)。如果是无序的,通常只能手动遍历。

17. 如何初始化带嵌套结构的结构体?

必须明确指定内部结构体的字段名,或者使用结构体字面量进行逐层初始化。Go 推荐组合优于继承。

18. 切片和数组的区别?

  • 数组:值类型,长度固定,是类型的一部分([3]int 和 [4]int 是不同类型)。传递时会进行内存拷贝。
  • 切片:引用类型,长度可变,底层指向数组。传递时成本极低(只拷贝 SliceHeader)。

19. new 和 make 的区别?

  • new:分配内存,置零,返回指针。适用于任意类型(如 new(int))。
  • make:分配并初始化,返回引用。仅用于 slice、map、channel。

20. Printf()、Sprintf()、Fprintf() 的区别?

  • Printf:输出到标准输出(屏幕)。
  • Sprintf:格式化并返回字符串。
  • Fprintf:输出到文件或 io.Writer。

21. 说说 go 语言中的 for 循环

Go 只有 for 关键字(没有 while)。支持传统 C 风格 for、类似 while 的条件 for 以及 for range。支持 continue 和 break。

22. Array 类型的值作为函数参数

Go 中数组是值传递。作为参数传递时,会完整拷贝整个数组。如果在函数内修改数组,外部原始数组不会改变。如果需要修改,请传切片或数组指针。

23. 说说 go 语言中的 switch 语句

默认不需要 break(匹配即结束)。
支持多条件匹配(case 1, 2:)。
支持类型断言 switch (switch v.(type))。

24. 说说 go 语言中有没有隐藏的 this 指针?

没有。Go 的方法接收者(Receiver)是显式声明的,可以是值或指针,名称由用户自己定义(通常用类型首字母),没有隐式的 this 或 self。

25. go 语言中的引用类型包含哪些?

切片(Slice)、字典(Map)、通道(Channel)、接口(Interface)、函数(Function)。

26. go 语言中指针运算有哪些?

Go 的指针不支持算术运算(如 p++)。可以通过 unsafe.Pointer 进行特殊转换,但通常不推荐。

27. go 语言触发异常的场景有哪些?

  • 空指针引用。
  • 数组/切片下标越界。
  • 除数为 0。
  • 对 nil map 赋值。
  • 关闭已关闭的 channel。
  • 显式调用 panic()。

28. 说说 go 语言的 beego 框架

一个全栈式的 Web 框架,集成了路由、ORM、日志、配置管理等,功能丰富,适合快速开发。

29. 说说 go 语言的 goconvey 框架

一个支持 BDD(行为驱动开发)风格的测试框架,拥有漂亮的 Web UI 界面,能实时监控代码变更并运行测试。

30. GoStub 的作用是什么?

用于单元测试中的打桩(Mock)。可以对全局变量、函数进行替换(Stub),以便隔离依赖进行测试。

31. go 语言编程的好处是什么?

  • 编译速度快,运行效率高。
  • 原生支持高并发(Goroutine)。
  • 语法简单,内置垃圾回收(GC)。
  • 强大的标准库和工具链。

32. 说说 go 语言的 select 机制

用于处理异步 IO 问题,专门用来监听多个 channel 的读写操作。
如果多个 case 同时满足,select 会随机选择一个执行。

33. 解释一下 go 语言中的静态类型声明

Go 是静态类型语言,变量在编译期必须确定类型。编译器会优化内存分配,但 Go 提供了类型推断(:=)让代码写起来像动态语言一样简洁。

34. go 的接口是什么?

接口定义了一组方法的集合。任何类型只要实现了这些方法,就被认为实现了该接口(隐式实现,Duck Typing)。接口是 Go 实现多态的关键。

35. Go 语言里面的类型断言是怎么回事?

用于检查接口变量底层存储的具体类型。
语法:value, ok := x.(T)。如果 x 不是类型 T,ok 为 false,不会 panic。

36. go 语言中局部变量和全局变量的缺省值是什么?

缺省值是该类型的零值:

  • 数值:0
  • 布尔:false
  • 字符串:“”
  • 指针/Slice/Map/Chan/Interface:nil

37. go 语言编程的好处是什么?(重复问题)

(同第 31 问)强调开发效率与运行效率的平衡,以及工程化友好的工具链。

38. 解释一下 go 语言中的静态类型声明(重复问题)

(同第 33 问)强调编译期检查能提前发现错误。

39. 模块化编程是怎么回事?

将大程序分解为小的、独立的包(Package)。Go 通过 package 和 import 管理依赖,Go Modules 是目前的标准包管理工具。

40. Golang 的方法有什么特别之处?

方法是绑定在特定类型(接收者)上的函数。
接收者可以是值或指针。这使得 Go 的非结构体类型(如 type MyInt int)也能定义方法。

41. Golang 可变参数

函数最后一个参数可以是可变参数,格式为 ...Type。本质上,它在函数内部是一个切片。调用时可以传多个值,或传切片后跟 ...。

42. Golang Slice 的底层实现

Slice 是一个结构体,包含三个字段:

  1. Pointer:指向底层数组的指针。
  2. Len:切片当前长度。
  3. Cap:切片当前容量。

43. Golang Slice 的扩容机制,有什么注意点?

  • Go 1.18 前:容量 < 1024 时翻倍;>= 1024 时增加 1.25 倍。
  • 注意:扩容可能会导致底层数组重新分配(地址改变)。如果切片引用了底层大数组的一小部分,可能会导致大数组无法被 GC 回收(内存泄漏)。

44. Golang Map 底层实现

Map 底层是 Hash Table。
核心结构是 hmap,包含多个 bmap(bucket,桶)。每个 bucket 存储 8 个键值对。使用链地址法解决哈希冲突,当 bucket 满时会链接溢出桶(overflow bucket)。

45. JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?

不一致。

  • nil slice 序列化为 null。
  • empty slice (make([]int, 0)) 序列化为 []。

46. Golang 的内存模型,为什么小对象多了会造成 GC 压力?

大量小对象意味着 GC 在标记阶段需要扫描更多的指针节点,增加了 CPU 消耗和 STW(Stop The World)的潜在时间。

47. Data Race 问题怎么解决?能不能不加锁解决这个问题?

  • 解决:使用 sync.Mutex 加锁,或者使用 Channel 通信。
  • 不加锁:可以使用 sync/atomic 包进行原子操作,或者通过 Channel 传递数据的所有权(避免共享)。

48. 在 range 迭代 slice 时,你怎么修改值的?

range 迭代出的 val 是值的副本,修改它不会影响原 slice。
正确做法:使用索引访问 slice[i] = newValue。

49. nil interface 和 nil interface value 的区别

这是 Interface 最大的坑。
Interface 只有当 Type 和 Value 都为 nil 时,才等于 nil。
如果将一个值为 nil 的具体类型指针赋值给 interface,该 interface 不等于 nil(因为 Type 有值)。

50. select 可以用于什么?

  • 多路复用:同时监听多个 channel 的读写。
  • 超时控制:配合 time.After 实现超时。
  • 非阻塞读写:配合 default 分支。
  • 完美退出:监听退出信号 channel。