Skip to content

Go函数 ​

阅读提示

  • 适用版本:Go 1.21+(示例在 Go 1.22 下验证)· 前置知识:变量 · 阅读时长:约 7 分钟

在 Go 中,函数是一等公民,函数是 Go 最基础的组成部分,也是 Go 的核心。

声明 ​

函数的声明格式如下

go
func 函数名([参数列表]) [返回值] {
  函数体
}

声明函数有两种办法,一种是通过func关键字直接声明,另一种就是通过var关键字来声明,如下所示

go
func sum(a int, b int) int {
  return a + b
}

var sum = func(a int, b int) int {
  return a + b
}

函数签名由函数名称,参数列表,返回值组成,下面是一个完整的例子,函数名称为Sum,有两个int类型的参数a,b,返回值类型为int。

go
func Sum(a int, b int) int {
   return a + b
}

还有一个非常重要的点,即 Go 中的函数不支持重载,像下面的代码就无法通过编译

go
type Person struct {
  Name    string
  Age     int
  Address string
  Salary  float64
}

func NewPerson(name string, age int, address string, salary float64) *Person {
  return &Person{Name: name, Age: age, Address: address, Salary: salary}
}

func NewPerson(name string) *Person {
  return &Person{Name: name}
}

Go 的理念便是:如果签名不一样那就是两个完全不同的函数,那么就不应该取一样的名字,函数重载会让代码变得混淆和难以理解。这种理念是否正确见仁见智,至少在 Go 中你可以仅通过函数名就知道它是干什么的,而不需要去找它到底是哪一个重载。

参数 ​

Go 中的参数名可以不带名称,一般这种是在接口或函数类型声明时才会用到,不过为了可读性一般还是建议尽量给参数加上名称

go
type ExWriter func(io.Writer) error

type Writer interface {
  ExWrite([]byte) (int, error)
}

对于类型相同的参数而言,可以只需要声明一次类型,不过条件是它们必须相邻

go
func Log(format string, a1, a2 any) {
  ...
}

变长参数可以接收 0 个或多个值,必须声明在参数列表的末尾,最典型的例子就是fmt.Printf函数。

go
func Printf(format string, a ...any) (n int, err error) {
  return Fprintf(os.Stdout, format, a...)
}

值得一提的是,Go 中的函数参数是传值传递,即在传递参数时会拷贝实参的值。如果你觉得在传递切片或 map 时会复制大量的内存,我可以告诉你大可不必担心,因为这两个数据结构本质上都是指针。

返回值 ​

下面是一个简单的函数返回值的例子,Sum函数返回一个int类型的值。

go
func Sum(a, b int) int {
   return a + b
}

当函数没有返回值时,不需要void,不带返回值即可。

go
func ErrPrintf(format string, a ...any) {
  _, _ = fmt.Fprintf(os.Stderr, format, a...)
}

Go 允许函数有多个返回值,此时就需要用括号将返回值围起来。

go
func Div(a, b float64) (float64, error) {
  if a == 0 {
    return math.NaN(), errors.New("0不能作为被除数")
  }
  return a / b, nil
}

Go 也支持具名返回值,不能与参数名重复,使用具名返回值时,return关键字可以不需要指定返回哪些值。

go
func Sum(a, b int) (ans int) {
  ans = a + b
  return
}

和参数一样,当有多个同类型的具名返回值时,可以省略掉重复的类型声明

go
func SumAndMul(a, b int) (c, d int) {
  c = a + b
  d = a * b
  return
}

不管具名返回值如何声明,永远都是以return关键字后的值为最高优先级。

go
func SumAndMul(a, b int) (c, d int) {
  c = a + b
  d = a * b
    // c,d将不会被返回
  return a + b, a * b
}

匿名函数 ​

匿名函数就是没有签名的函数,例如下面的函数func(a, b int) int,它没有名称,所以我们只能在它的函数体后紧跟括号来进行调用。

go
func main() {
   func(a, b int) int {
      return a + b
   }(1, 2)
}

在调用一个函数时,当它的参数是一个函数类型时,这时名称不再重要,就可以直接传递一个匿名函数,如下所示

go
type Person struct {
  Name   string
  Age    int
  Salary float64
}

func main() {
  people := []Person{
    {Name: "Alice", Age: 25, Salary: 5000.0},
    {Name: "Bob", Age: 30, Salary: 6000.0},
    {Name: "Charlie", Age: 28, Salary: 5500.0},
  }

  slices.SortFunc(people, func(p1 Person, p2 Person) int {
    if p1.Name > p2.Name {
      return 1
    } else if p1.Name < p2.Name {
      return -1
    }
    return 0
  })
}

这是一个自定义排序规则的例子,slices.SortFunc接受两个参数,一个是切片,另一个就是比较函数,不考虑复用的话,我们就可以直接传递匿名函数。

闭包 ​

闭包(Closure)这一概念,在一些语言中又被称为 Lambda 表达式,与匿名函数一起使用,闭包 = 函数 + 环境引用吗,看下面一个例子:

go
func main() {
  grow := Exp(2)
  for i := range 10 {
    fmt.Printf("2^%d=%d\n", i, grow())
  }
}

func Exp(n int) func() int {
  e := 1
  return func() int {
    temp := e
    e *= n
    return temp
  }
}

输出

2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512

Exp函数的返回值是一个函数,这里将称成为grow函数,每将它调用一次,变量e就会以指数级增长一次。grow函数引用了Exp函数的两个变量:e和n,它们诞生在Exp函数的作用域内,在正常情况下随着Exp函数的调用结束,这些变量的内存会随着出栈而被回收。但是由于grow函数引用了它们,所以它们无法被回收,而是逃逸到了堆上,即使Exp函数的生命周期已经结束了,但变量e和n的生命周期并没有结束,在grow函数内还能直接修改这两个变量,grow函数就是一个闭包函数。

利用闭包,可以非常简单的实现一个求费波那契数列的函数,代码如下

go
func main() {
    // 10个斐波那契数
  fib := Fib(10)
  for n, next := fib(); next; n, next = fib() {
    fmt.Println(n)
  }
}

func Fib(n int) func() (int, bool) {
  a, b, c := 1, 1, 2
  i := 0
  return func() (int, bool) {
    if i >= n {
      return 0, false
    } else if i < 2 {
      f := i
      i++
      return f, true
    }

    a, b = b, c
    c = a + b
    i++

    return a, true
  }
}

输出为

0
1
1
2
3
5
8
13
21
34

延迟调用 ​

defer关键字可以使得一个函数延迟一段时间调用,在函数返回之前这些 defer 描述的函数最后都会被逐个执行,看下面一个例子

go
func main() {
  Do()
}

func Do() {
  defer func() {
    fmt.Println("1")
  }()
  fmt.Println("2")
}

输出

2
1

因为 defer 是在函数返回前执行的,你也可以在 defer 中修改函数的返回值

go
func main() {
  fmt.Println(sum(3, 5))
}

func sum(a, b int) (s int) {
  defer func() {
    s -= 10
  }()
  s = a + b
  return
}

当有多个 defer 描述的函数时,就会像栈一样先进后出的顺序执行。

go
func main() {
  fmt.Println(0)
  Do()
}

func Do() {
  defer fmt.Println(1)
  fmt.Println(2)
  defer fmt.Println(3)
  defer fmt.Println(4)
  fmt.Println(5)
}
0
2
5
4
3
1

延迟调用通常用于释放文件资源,关闭网络连接等操作,还有一个用法是捕获panic,不过这是错误处理一节中才会涉及到的东西。

循环 ​

虽然没有明令禁止,一般建议不要在 for 循环中使用 defer,如下所示

go
func main() {
  n := 5
  for i := range n {
    defer fmt.Println(i)
  }
}

输出如下

4
3
2
1
0

这段代码结果是正确的,但过程也许不对。在 Go 中,每创建一个 defer,就需要在当前协程申请一片内存空间。假设在上面例子中不是简单的 for n 循环,而是一个较为复杂的数据处理流程,当外部请求数突然激增时,那么在短时间内就会创建大量的 defer,在循环次数很大或次数不确定时,就可能会导致内存占用突然暴涨,这种我们一般称之为内存泄漏。

参数预计算 ​

对于延迟调用有一些反直觉的细节,比如下面这个例子

go
func main() {
  defer fmt.Println(Fn1())
  fmt.Println("3")
}

func Fn1() int {
  fmt.Println("2")
  return 1
}

这个坑还是非常隐晦的,笔者以前就因为这个坑,半天排查不出来是什么原因,可以猜猜输出是什么,答案如下

2
3
1

可能很多人认为是下面这种输出

3
2
1

按照使用者的初衷来说,fmt.Println(Fn1())这部分应该是希望它们在函数体执行结束后再执行,fmt.Println确实是最后执行的,但Fn1()是在意料之外的,下面这个例子的情况就更加明显了。

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer fmt.Println(sum(a, b))
  a = 3
  b = 4
}

func sum(a, b int) int {
  return a + b
}

它的输出一定是 3 而不是 7,如果使用闭包而不是延迟调用,结果又不一样了

go
func main() {
  var a, b int
  a = 1
  b = 2
  f := func() {
    fmt.Println(sum(a, b))
  }
  a = 3
  b = 4
  f()
}

闭包的输出是 7,那如果把延迟调用和闭包结合起来呢

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer func() {
    fmt.Println(sum(a, b))
  }()
  a = 3
  b = 4
}

这次就正常了,输出的是 7。下面再改一下,没有闭包了

go
func main() {
  var a, b int
  a = 1
  b = 2
  defer func(num int) {
    fmt.Println(num)
  }(sum(a, b))
  a = 3
  b = 4
}

输出又变回 3 了。通过对比上面几个例子可以发现这段代码

defer fmt.Println(sum(a,b))

其实等价于

defer fmt.Println(3)

go 不会等到最后才去调用sum函数,sum函数早在延迟调用被执行以前就被调用了,并作为参数传递了fmt.Println。总结就是,对于defer直接作用的函数而言,它的参数是会被预计算的,这也就导致了第一个例子中的奇怪现象,对于这种情况,尤其是在延迟调用中将函数返回值作为参数的情况尤其需要注意。

完整示例 ​

下面的程序可以直接运行:复制到文件后执行 go run main.go,或粘贴到 Go Playground 运行。

go
package main

import (
	"errors"
	"fmt"
)

// 多返回值:结果、余数、错误
func divmod(a, b int) (int, int, error) {
	if b == 0 {
		return 0, 0, errors.New("division by zero")
	}
	return a / b, a % b, nil
}

// 变参函数与命名返回值
func sum(nums ...int) (total int) {
	for _, n := range nums {
		total += n
	}
	return
}

// 闭包会保留自己的状态
func counter() func() int {
	i := 0
	return func() int {
		i++
		return i
	}
}

func main() {
	q, r, err := divmod(7, 2)
	fmt.Println(q, r, err)

	fmt.Println(sum(1, 2, 3))

	next := counter()
	fmt.Println(next(), next(), next())

	defer fmt.Println("deferred runs last")
	fmt.Println("main body")
}

预期输出:

text
3 1 <nil>
6
1 2 3
main body
deferred runs last

常见错误与踩坑 ​

  • 函数可以当值传递,但只能与 nil 比较。 f == g 无法编译。
  • 变参本质是切片。 转发切片要写 sum(nums...);sum() 不传参数也是合法的。
  • 命名返回值配合 defer 会改变最终返回值。 延迟闭包若写入命名返回值,会覆盖之前的 return,用于错误包装很方便,也容易被误读。
  • defer 的参数在注册时求值。 defer fmt.Println(sum(1, 2)) 在注册时就会计算 sum,而不是函数退出时。
  • 切片和 map 是"引用语义的值传递"。 向切片参数 append 可能对调用方不可见,但改元素是可见的。

更新记录 ​

  • 2026-10-14:新增"阅读提示"、完整可运行示例(含预期输出)、常见错误与踩坑清单,以及本更新记录。