Go 语言基础
Go 语言设计、值语义、初始化、defer、字符串与 unsafe 高频面试题。
Go 相比其他语言有哪些特点?
回答重点
Go 的优势不在于某一项能力绝对领先,而在于工程取舍比较统一:
- 语法和语言特性相对精简,代码格式由
gofmt统一。 - 编译速度快,可以直接生成便于部署的本地可执行文件。
- goroutine、channel 和运行时调度器降低了并发编程的门槛。
- 标准库覆盖网络、HTTP、测试、性能分析等常见服务端需求。
- 内置垃圾回收、竞态检测、基准测试和性能分析工具。
需要避免“Go 的 GC 一定比 Java 更快”这类绝对结论。GC 表现取决于堆大小、对象生命周期、延迟目标、运行时版本和具体负载。
goroutine、线程和进程有什么区别?
回答重点
- 进程拥有独立的虚拟地址空间和操作系统资源,是资源隔离的边界。
- 线程是操作系统调度执行的基本单位,同一进程内的线程共享地址空间。
- goroutine是 Go 运行时管理的轻量级执行单元。运行时通过 GMP 调度器把大量 goroutine 复用到较少的操作系统线程上。
goroutine 的栈会按需增长和收缩,创建与切换成本通常低于操作系统线程。但 goroutine 并不会让共享数据自动安全,多个 goroutine 访问同一份可变状态时仍需 channel、锁或原子操作建立同步关系。
make 和 new 有什么区别?
回答重点
new(T)为T的零值分配存储空间,返回*T。make(T, ...)只适用于 slice、map 和 channel,返回初始化后的T,而不是*T。
new 得到的值已经按零值初始化,不能说成“只分配但不初始化”。区别在于 slice、map 和 channel 的零值通常还不能完成预期操作,因此需要 make 初始化其运行时结构。
package main
import "fmt"
func main() {
p := new(int)
s := make([]int, 3)
m := make(map[string]int)
ch := make(chan int, 1)
fmt.Println(*p, s, len(m), cap(ch))
}Go 1.22 前后循环变量的语义有什么变化?
回答重点
对于语言版本为 Go 1.22 及以上的包,for 循环中声明的迭代变量每轮都会创建新实例,闭包捕获和取地址不再共享同一个循环变量。
Go 1.21 及更早语言版本中,循环变量在各轮之间复用,因此把变量地址保存到切片、或让闭包异步读取变量,可能全部观察到同一个变量的最终值。
package main
import "fmt"
func main() {
values := []string{"a", "b", "c"}
for _, value := range values {
fmt.Printf("%p %s\n", &value, value)
}
}这个变化由模块的 go 版本决定,不应误写成“Go 1.23 才变化”。参考 Go 1.22 发布说明。
如何高效拼接字符串?
回答重点
- 少量固定字符串使用
+,可读性最好,编译器也能做优化。 - 循环追加或增量构造字符串优先使用
strings.Builder。 - 已有字符串切片并需要分隔符时使用
strings.Join。 - 同时处理
[]byte或需要读写缓冲区时使用bytes.Buffer。 fmt.Sprintf适合格式化,不应作为纯拼接的默认方案。
不存在脱离输入规模和 Go 版本的固定性能排名。性能敏感时应使用基准测试,并在能够估算最终大小时调用 Grow。
package main
import (
"fmt"
"strings"
)
func main() {
parts := []string{"Go", " ", "interview"}
var builder strings.Builder
builder.Grow(len("Go interview"))
for _, part := range parts {
builder.WriteString(part)
}
fmt.Println(builder.String())
fmt.Println(strings.Join(parts, ""))
}defer 的执行顺序和求值时机是什么?
回答重点
defer按注册顺序的逆序执行,即后进先出。- 被延迟函数及其参数在执行到
defer语句时就会求值。 - 延迟调用在函数返回前执行,包括正常返回和当前 goroutine 发生 panic 后展开栈的过程。
- 延迟函数可以读取或修改命名返回值。
os.Exit会直接终止进程,不会执行defer。
package main
import "fmt"
func result() (n int) {
defer func() { n++ }()
return 1
}
func main() {
fmt.Println(result()) // 2
}byte、rune 和字符串是什么关系?
回答重点
Go 字符串是只读字节序列,通常承载 UTF-8 文本,但语言并不要求字符串内容必须是合法 UTF-8。
byte是uint8的别名,表示一个字节。rune是int32的别名,通常表示一个 Unicode 码点。len(s)返回字节数。utf8.RuneCountInString(s)或len([]rune(s))返回码点数量,不等于用户看到的字符数量。组合字符和 emoji 可能由多个码点组成。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "hello 你好"
fmt.Println(len(s)) // 12
fmt.Println(utf8.RuneCountInString(s)) // 8
}结构体标签有什么作用?
回答重点
结构体标签是附着在字段声明上的字符串元数据。标准库或框架可以通过反射读取它们,例如 encoding/json 使用 json 标签控制字段名和序列化行为。
标签本身不会自动产生效果,必须由相应代码解析。标签只对可导出字段有实际价值,因为反射和编码包通常无法设置未导出字段。
type User struct {
ID int `json:"id"`
Password string `json:"-"`
}%v、%+v 和 %#v 有什么区别?
回答重点
%v使用默认格式输出值。%+v输出结构体时包含字段名。%#v输出值的 Go 语法表示,通常还包含类型信息。
这些格式的具体输出取决于值是否实现 fmt.Formatter、fmt.Stringer 或 error。
空结构体 struct{} 是否占空间?有什么用途?
回答重点
unsafe.Sizeof(struct{}{}) 为 0。常见用途包括:
- 作为
map[T]struct{}的值来表达集合。 - 通过
chan struct{}只传递事件,不携带业务数据。 - 作为只承载方法的类型。
零尺寸值在数组、切片或结构体中的实际地址布局由实现决定,不能据此假设不同零尺寸变量一定有不同地址。
package main
import "fmt"
func main() {
set := map[string]struct{}{"Go": {}}
_, exists := set["Go"]
fmt.Println(exists)
}init 函数什么时候执行?
回答重点
程序启动时,运行时先按依赖顺序初始化包。每个包先初始化包级变量,再按源文件顺序执行该包中的 init 函数;依赖包完成后才会初始化当前包,最后调用 main.main。
init 不能显式调用,不能有参数或返回值,一个文件可以声明多个 init。同一包多个文件的提交顺序由构建系统决定,因此不应让不同文件中的 init 隐式依赖彼此顺序。
Go 函数参数是值传递还是引用传递?
回答重点
Go 只有值传递。传入数组、结构体、指针、slice、map、channel 或 interface 时,都会复制实参的值。
容易混淆的地方是:slice、map、channel、指针等值内部包含对共享数据的引用。复制这些描述符后,调用方与被调用方仍可能访问同一底层数据,但被调用方重新给自己的参数赋值不会改变调用方变量本身。
如何判断变量分配在栈还是堆?
回答重点
栈或堆是编译器实现决策,不由 new、make 或源码写法直接决定。编译器根据值的生命周期、大小和使用方式执行逃逸分析,而且结果可能随 Go 版本变化。
可以用编译器诊断查看当前构建下的决定:
go build -gcflags='-m=2' ./...“返回局部变量地址就一定性能差”也是误区。Go 会保证地址有效,是否值得优化要结合分配次数、对象大小和性能分析结果判断。
Go 的多返回值如何实现?
回答重点
语言规范只保证函数可以返回多个值,不规定必须通过调用方栈帧上的连续空间实现。具体传递方式属于编译器 ABI,可能使用寄存器,也可能使用栈,且会随架构和工具链变化。
面试中应先说明语言语义,再把寄存器或栈上的传递称为“当前编译器实现”,不要当作 Go 语言保证。
空白标识符 _ 有什么作用?
回答重点
空白标识符用于显式忽略值,常见场景包括:
- 忽略多返回值中的某一项。
- 忽略
range的索引或元素。 - 使用空白导入触发包的初始化副作用。
- 在编译期声明接口实现关系。
package main
import "io"
type reader struct{}
func (reader) Read([]byte) (int, error) { return 0, io.EOF }
var _ io.Reader = (*reader)(nil)
func main() {}普通指针、unsafe.Pointer 和 uintptr 有什么区别?
回答重点
*T带有静态类型,编译器会检查可执行的操作。unsafe.Pointer可以在不同指针类型之间转换,并仍被垃圾回收器视为指针。uintptr只是足以容纳地址的整数。它不会让对象保持存活,也不能长期保存后再安全转回指针。
使用 uintptr 做地址运算时,转换通常必须出现在同一个表达式中,并遵守 unsafe 包文档列出的合法模式。涉及对象生命周期时可配合 runtime.KeepAlive,但它不能把任意不安全转换变成安全代码。
评论