boxmoe_header_banner_img

Hello! 欢迎来到我的博客!

加载中

文章导读

Go function – 有关function我能告诉你的一切


avatar
xiaoifei 2026年5月7日 7

官方参考:

写 Go 的时候,函数是最先见到、也最容易被低估的角色。
它表面上只是 func 关键字后面跟一段代码,但真正写起来你会发现:Go 把函数设计得非常“务实”。它可以有多个返回值,可以被当作值传来传去,可以临时组装成闭包,还能摇身一变成为某个类型的方法。下面就来一起了解function有哪些基础的使用方法以及技巧

1. Function 基础

Go 里函数用 func 关键字定义:

func 函数名(参数列表) 返回值类型 {
    // 函数体
}

例如:

func add(a int, b int) int {
    return a + b
}
// 可以将同类型参数合并声明
func add(a, b int) int {
    return a + b
}

1.1 多返回值

多返回值是 Go 很有辨识度的特性。
在很多语言里,一个函数想返回两个结果,往往要塞进对象、数组、元组,或者靠异常去表达错误。Go 更直接:既然函数可能天然产出多个东西,那就把它们摊开放在返回值里。

func divide(a, b int) (int, int) {
    return a / b, a % b
}
func main() {
    q, r := divide(10, 3)
    q2, _ := divide(10, 3) // 也可以忽略某个返回值:
}

1.2 命名返回值(Go独有)

返回值可以提前命名。
这有点像在函数出口提前摆好几个盒子,函数体里只负责往盒子里放东西,最后一句 return 就能把盒子一起端出去:

func rectangle(w, h int) (area int) {
    area = w * h
    return
}
func rectangle(w, h int) int { // 等价
    area := w * h
    return area
}

多返回值也可以命名:

func calc(a, b int) (sum int, diff int) {
    sum = a + b
    diff = a - b
    return
}

不过实际开发里不要滥用裸 return,函数长了会降低可读性。
此外再遇上defer+闭包的情况,可能会产生一些意想不到的输出结果

func test() (result int) {
    defer func() {
        result++
    }()
    return 0
}

很多人第一反应是0,但它的实际输出是1

官方文档这样解释:

For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned. If the deferred function has any return values, they are discarded when the function completes. (See also the section on handling panics.)
例如,如果延迟函数是一个函数字面量 ,并且其外层函数具有命名的结果参数 ,且这些参数的作用域位于该字面量内部,则延迟函数可以在结果参数返回之前访问并修改它们。如果延迟函数有任何返回值,则这些返回值会在函数执行完毕时被丢弃。(另请参阅关于处理 panic 的部分。)

2. Function 核心用法

2.1 main 与 init 函数

Go 程序启动时,并不是一上来就冲进 main
它会先把包依赖、全局变量、init 函数这些“开场准备”处理完,再把舞台交给 main

func main() {
    fmt.Println("Hello Go")
}
func init() {
    fmt.Println("init")
}

程序运行时会先初始化包依赖,执行所有 init,最后执行 main

对比点initmain
是否自动执行
是否建议手动调用
数量多个只能一个
执行时机程序启动前最后执行
所在包任意包只能 main 包
用途初始化程序入口

2.2 立即执行函数

对于写过 Js,对这一套应该很眼熟。函数刚定义完,后面立刻跟一对括号执行。

func() {
    fmt.Println("run immediately")
}()

func(name string) {
    fmt.Println("Hello", name)
}("XiaoYi")

它在并发示例中经常出现,用来保证每个 goroutine 拿到独立变量,避免闭包捕获同一个 i
在并发场景里相当于给变量拍一张当时的快照:

for i := 0; i < n; i++ {
    go func(i int) {
        // 使用 i
    }(i)
}

2.3 函数也是值

这个用法让我想到了C++的函数指针传递函数,总之这一套函数操作自由度极高

Go 的函数可以作为值传递。

更有意思的是,当函数引用了外部变量时,它带走的不只是函数本体,还包括它需要的那部分环境。
这就是闭包的味道:函数像是背了一个小包,把路上需要的变量也一起带着。

函数可以赋值给变量:

add := func(a, b int) int { // 匿名函数作为值传递
    return a + b
}

result := add(1, 2)

要注意的是内置函数(例如slice的len,cap等)不能作为值在变量之间传递:

Built-in functions
The built-in functions do not have standard Go types, so they can only appear in call expressions; they cannot be used as function values.

2.3.1 函数作为参数

函数作为参数时,我们可以把“要做什么”交给调用方决定。
外层函数只负责搭台,真正的计算逻辑由传进来的函数登场:

func operate(a, b int, fn func(int, int) int) int {
    return fn(a, b)
}

result := operate(3, 4, func(x, y int) int {
    return x + y
})

2.3.2 函数作为返回值

函数作为返回值时,外层函数就像一个工厂:根据传入参数,生产出一个定制版函数。

func makeAdder(base int) func(int) int {
    return func(x int) int {
        return base + x
    }
}

使用:

add10 := makeAdder(10)
fmt.Println(add10(5)) // 15

这里内部函数引用了外部变量 base
makeAdder(10) 返回的不是一个普通加法器,而是一个已经记住 base = 10 的加法器,这就是闭包。

2.4 闭包

闭包可以记住外部变量。
如果普通函数像一次性工具,用完就散场;闭包更像带记忆的工具,它能把某些状态留在身上,下次调用时继续用。

func counter() func() int {
    count := 0

    return func() int {
        count++
        return count
    }
}

使用:

c := counter()

fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3

这里的 count 明明定义在 counter 里面,但 counter 返回以后它并没有立刻消失。
因为返回的匿名函数还需要它,所以这个变量会继续活着。每次调用 c(),都是在修改同一个被记住的 count

2.5 defer 让函数延迟执行

defer 标记的函数会在当前函数结束前执行
它特别适合做“收尾动作”:打开文件后记得关闭,加锁后记得解锁,申请资源后记得释放。

func readFile() {
    defer fmt.Println("close file")

    fmt.Println("read file")
}

输出:

read file
close file

多个 defer 是后进先出,可以想象成栈的结构

func test() {
    defer fmt.Println("A")
    defer fmt.Println("B")
    defer fmt.Println("C")
}

输出:

C
B
A

3. Function 进阶

3.1 面向对象

Go 没有 class 关键字,但这并不影响它表达“对象行为”。
它没有把方法塞进类里,而是把方法绑定到类型上:类型负责数据,方法负责行为,两者组合起来也能写出清晰的对象模型。

对象方法:本质是带接收者的函数

官方文档:Method_declarations

Go 可以给类型定义方法:

MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver   = Parameters .

官方定义里面,将接收者插在了 funcMethodName 之间。
下面这段代码看起来像成员方法,但本质仍然是一种带接收者的函数:

type Player struct {
    Name string
    HP   int
}

func (p Player) SayName() {
    fmt.Println(p.Name)
}

调用:

player := Player{Name: "Knight", HP: 100}
player.SayName()

下面有几点注意事项:

  1. receiver 只能有一个,不能是可变参数。
  2. receiver 必须绑定到当前包里的 defined type。
  3. 方法名绑定到 receiver base type,不是绑定到某个实例。
  4. 泛型类型可以有方法,但 receiver 的类型参数声明有严格规则。

值接收者和指针接收者是 Go 方法里非常关键的一道分水岭。
值接收者会复制一份对象,像是拿到一份复印件;指针接收者拿到的是原对象的地址,修改会真正落到原对象身上。
可以通过下面一个例子来证明:

type Player struct {
	HP int
}

func (p Player) ValueChange() {
	p.HP = 0
}

func (p *Player) PointerChange() {
	p.HP = 0
}
func main() {
	p := Player{HP: 100}

	p.ValueChange()
	fmt.Println(p.HP) // 100

	p.PointerChange()
	fmt.Println(p.HP) // 0
}

虽然方法接收者是 *Player,但 Go 通常允许你直接用 player.TakeDamage(),不用手动写 (&player).TakeDamage()
这是 Go 在可寻址变量上提供的一点语法照顾,写起来更像普通方法调用:

Effective Go: methods
值接收者方法:T*T都能调用
指针接收者方法:原则上只有 *T 能调用
但如果 T 是“可寻址的变量”,Go 会自动帮你取地址

3.2 可变参数函数

可变参数函数适合处理“参数数量不固定”的场景。
语法是 ...类型,意思是:这里可以接 0 个、1 个,也可以接很多个同类型参数。

func sum(nums ...int) int {
    total := 0

    for _, n := range nums {
        total += n
    }

    return total
}

调用:

sum(1, 2, 3)
sum()

如果已经有 slice,可以用 ... 展开。
这一步有点像把一整盒零件倒出来,一个个交给函数:

nums := []int{1, 2, 3}
sum(nums...)

注意:可变参数必须放在参数列表最后。

func log(prefix string, values ...int) {
}

3.3 函数类型:给函数起一个别名

可以给函数签名起别名,让意图更清楚:

type Operation func(int, int) int

func calculate(a, b int, op Operation) int {
    return op(a, b)
}

4. 小结

到这里,Go function 的主要地图基本展开了。
它不是只有“输入参数,返回结果”这么简单,而是一整套围绕代码复用、行为传递、状态保存、资源收尾组织起来的工具。

看看下面这些函数写法,你是否都能一眼读出含义?

func f()
func f(a int)
func f(a, b int)
func f(a int, b string)
func f() int
func f() (int, string)
func f(a int) (result int)
func f(nums ...int)
func f(fn func(int) int)
func f() func(int) int

如果前面的内容都能读懂,再看下面这个综合案例就会顺很多。

package main

import "fmt"

type Player struct {
    Name string
    HP   int
}

func NewPlayer(name string) *Player {
    return &Player{
        Name: name,
        HP:   100,
    }
}

func (p *Player) TakeDamage(damage int) {
    p.HP -= damage
}

func (p Player) IsAlive() bool {
    return p.HP > 0
}

func main() {
    player := NewPlayer("Cat Knight")

    player.TakeDamage(30)

    fmt.Println(player.Name)
    fmt.Println(player.HP)
    fmt.Println(player.IsAlive())
}

输出:

Cat Knight
70
true


评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码