Skip to content

第04章 面试题精选 —— 函数


★ 基础题

Q1:Go 函数和其他语言函数最大的不同?

  1. 多返回值:天然返回 (value, error)
  2. 没有默认参数:要默认值得自己写"包装函数"或用 option 模式
  3. 没有函数重载:同一个包里不能有同名函数
  4. 不能嵌套定义:函数里不能 func foo() 但可以用匿名函数赋值给变量

Q2:下面代码输出什么?

go
func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    fmt.Println("hello")
}

hello
3
2
1

defer 是 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) 中的 xdefer 这一行立即求值,被复制为 10。

如果改成闭包:

go
defer func() { fmt.Println(x) }()

就会输出 20,因为闭包捕获的是变量本身。


Q6:Go 函数传参是值传递还是引用传递?

:Go 永远是值传递

但是:

  • slicemapchannel*T(指针)时,值本身是个引用,改动可见
  • intstringstruct{...} 时,是真正的拷贝
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

详细执行步骤

  1. return 5x = 5 写入命名返回值
  2. 执行所有 defer:x++ → x=6
  3. 真正返回,调用者拿到 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;闭包捕引用;函数即值"


🎯 自测练习

  1. 写一个函数 min(nums ...int) int,返回最小值
  2. 用闭包实现一个"斐波那契生成器",每次调用返回下一个数
  3. 用 functional options 写一个 NewLogger(opts ...Option) 的初始化函数
  4. 解释下面这段代码为什么 panic:
    go
    defer func() { panic("oops") }()

进入第05章 🚀