主题
第04章 面试题精选 —— 函数
★ 基础题
Q1:Go 函数和其他语言函数最大的不同?
- 多返回值:天然返回
(value, error) - 没有默认参数:要默认值得自己写"包装函数"或用 option 模式
- 没有函数重载:同一个包里不能有同名函数
- 不能嵌套定义:函数里不能
func foo()但可以用匿名函数赋值给变量
Q2:下面代码输出什么?
go
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("hello")
}答:
hello
3
2
1defer 是 LIFO 栈。
Q3:解释 func sum(nums ...int) 中的 ...。
...int 表示可变参数。在函数内 nums 的类型是 []int。
调用方式:
go
sum(1, 2, 3) // 传若干 int
sum() // 也可以一个不传
sum([]int{1,2,3}...) // 切片用 ... 展开传入Q4:什么是命名返回值?什么时候用?
go
func split(sum int) (x, y int) {
x = sum / 2
y = sum - x
return // 裸返回,等同于 return x, y
}适用场景:
- 函数短(5 行内)
- 返回值需要在 defer 里被修改(如错误处理)
不适用场景:函数长,会让人忘记裸返回的 return 实际返回什么。
★★ 进阶题
Q5:下面代码输出什么?
go
func main() {
x := 10
defer fmt.Println("defer:", x)
x = 20
fmt.Println("now:", x)
}答:
now: 20
defer: 10原因:defer fmt.Println(x) 中的 x 在 defer 这一行立即求值,被复制为 10。
如果改成闭包:
go
defer func() { fmt.Println(x) }()就会输出 20,因为闭包捕获的是变量本身。
Q6:Go 函数传参是值传递还是引用传递?
答:Go 永远是值传递。
但是:
- 传
slice、map、channel、*T(指针)时,值本身是个引用,改动可见 - 传
int、string、struct{...}时,是真正的拷贝
go
func modify(s []int) {
s[0] = 100 // 影响外部
}
func reassign(s []int) {
s = []int{99} // 不影响外部!只换了局部副本
}Q7:闭包与 for 循环的"经典坑"是什么?
go
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() { fmt.Println(i) })
}
for _, f := range funcs { f() }- Go 1.21 及之前:输出
3 3 3(共享 i) - Go 1.22 起:输出
0 1 2(每次循环 i 都是新变量)
老版本修复方法:
go
for i := 0; i < 3; i++ {
i := i // 重新声明
funcs = append(funcs, func() { fmt.Println(i) })
}Q8:defer 和 return 的执行顺序?
go
func f() (x int) {
defer func() { x++ }()
return 5
}答:返回 6。
详细执行步骤:
return 5把x = 5写入命名返回值- 执行所有 defer:
x++→ x=6 - 真正返回,调用者拿到 6
关键:defer 在 return 设置返回值之后、真正返回之前执行。
Q9:Go 的 defer 性能开销大吗?
| 版本 | 单次 defer 开销 |
|---|---|
| Go 1.13 之前 | 比直接调用慢 30-50 倍 |
| Go 1.14+ | 几乎无开销(内联到栈上) |
结论:现代 Go 中 defer 开销可忽略,放心用。
Q10:写一个限流器:每秒最多调用 N 次。
go
func RateLimit(n int, interval time.Duration) func() bool {
bucket := make(chan struct{}, n)
go func() {
t := time.NewTicker(interval / time.Duration(n))
for range t.C {
select {
case bucket <- struct{}{}:
default:
}
}
}()
return func() bool {
select {
case <-bucket:
return true
default:
return false
}
}
}
allow := RateLimit(10, time.Second)
if allow() { /* 处理请求 */ }考察点:闭包 + channel + goroutine 协作。
★★★ 深度题
Q11:函数返回 slice/map 后,外部修改会影响内部吗?
go
func get() []int {
return []int{1, 2, 3}
}
s := get()
s[0] = 100答:内部已无人持有这个 slice 的引用,所以"影响"无意义。
但如果是从 receiver 返回:
go
type S struct { data []int }
func (s *S) Get() []int { return s.data }
s := &S{data: []int{1,2,3}}
got := s.Get()
got[0] = 100
fmt.Println(s.data[0]) // 100,被外部修改了!防御方法:返回拷贝
go
return append([]int(nil), s.data...)Q12:什么是 functional options 模式?
Go 不支持默认参数和重载,但可以用函数式选项优雅地实现可选参数:
go
type Server struct {
addr string
timeout time.Duration
tls bool
}
type Option func(*Server)
func WithTimeout(t time.Duration) Option {
return func(s *Server) { s.timeout = t }
}
func WithTLS() Option {
return func(s *Server) { s.tls = true }
}
func New(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 10*time.Second}
for _, opt := range opts {
opt(s)
}
return s
}
s := New("0.0.0.0:80", WithTimeout(time.Minute), WithTLS())这是 Go 社区最经典的设计模式之一,gRPC、etcd 都在用。
📌 速记口诀
"多返回值 + error;defer LIFO;闭包捕引用;函数即值"
🎯 自测练习
- 写一个函数
min(nums ...int) int,返回最小值 - 用闭包实现一个"斐波那契生成器",每次调用返回下一个数
- 用 functional options 写一个
NewLogger(opts ...Option)的初始化函数 - 解释下面这段代码为什么 panic:go
defer func() { panic("oops") }()
进入第05章 🚀