Go 入门(02):函数与错误处理——多返回值、命名返回值、error 惯例
更新时间:2026-09-01。本文是
languages/go/beginner/入门层第 2 篇,接 语法基础。函数是 Go 的一等公民,除了基本语法,Go 在多返回值和错误处理上有自己鲜明的风格,本篇讲透。
本文要回答的问题
- Go 函数怎么定义?参数和返回值语法和 C 有什么不一样?
- 多返回值有什么用?为什么 Go 把它做进语言核心?
- 命名返回值是什么?什么时候该用?
- Go 的错误处理为什么是"返回 error"而不是异常?怎么写才是 Go 风格?
- 闭包在 Go 里怎么写?和其他语言一样吗?
一、函数基本语法
Go 函数定义:
func 函数名(参数列表) 返回值列表 {
// 函数体
return 返回值
}最简单的加法:
func add(a int, b int) int {
return a + b
}参数类型简写——同类型可以只写一次:
func add(a, b int) int {
return a + b
}没有返回值就是 void 语义,省略返回值:
func printHello(name string) {
fmt.Printf("Hello, %s!\n", name)
}调用和 C/Java 一样:sum := add(3, 5)。
二、多返回值:Go 的核心特色
Go 允许函数直接返回多个值,这不是语法糖,是语言核心设计——专门为了让错误处理变得简单:
func div(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero: a=%d", a)
}
return a / b, nil
}调用时一次接两个:
v, err := div(10, 2)
if err != nil {
// 处理错误
log.Fatal(err)
}
fmt.Println("result:", v)这就是 Go 最常见的写法:一个返回值放正常结果,一个放错误,一目了然。
和 C 的对比:C 只能返回一个值,错误得用输出参数(指针)或者全局变量
errno,都不如多返回值直接。和 C++/Java 对比:它们用异常,Go 选择"错误是值",显式返回,简单直接。
空白标识符 _:如果你不关心某个返回值,用 _ 扔掉:
result, _ := div(10, 2) // 忽略错误,不推荐,但语法允许三、命名返回值:给返回值起名字
Go 允许你给返回值命名,它们会在函数入口被初始化为零值:
func div(a, b int) (res int, err error) {
if b == 0 {
err = fmt.Errorf("division by zero")
return // naked return:直接返回,res 还是默认 0
}
res = a / b
return // 这里也直接返回,因为 res 和 err 已经赋值了
}这叫裸返回(naked return),短函数能用,长函数别用——会让可读性变差。
命名返回值适合:
- 短函数,多个返回值能省一点代码
- 要给文档注释说明每个返回值的含义
- 闭包修改返回值(少见)
别在大函数里用裸返回,会晕。
四、Go 错误处理:error 是值,不是异常
这是 Go 和很多语言最大的区别:Go 没有异常机制(try/catch/throw),错误就是普通返回值,类型是 error:
type error interface {
Error() string
}error 就是个只有 Error() string 方法的接口,任何类型实现这个方法就能当错误用。最常用的创建方式:
import "fmt"
err := fmt.Errorf("invalid value: %d", x) // 格式化错误信息Go 错误处理的惯例:
// 1. 先判断错误,错了就处理(返回或退出),成功再往下走
if err != nil {
return 0, err // 向上抛,让调用者处理
}
// 正确路径在最外层缩进,不用层层包在 else 里
// ... 正常逻辑 ...这叫尽早返回,和 C 里处理错误的风格一致,代码缩进少,读起来顺。
反例(别这么写):
// 坏:正确路径嵌在 if 里,缩进深
if err == nil {
// ... 一大段正常逻辑 ...
} else {
// 处理错误
}什么时候用 panic?
panic是 Go 里的"真正异常",用来处理真正不可恢复的错误,比如数组越界、空指针引用。- 业务逻辑错误(文件打不开、参数非法)一律返回
error,不要 panic。 - 程序启动时必要的依赖错了,可以
log.Fatalf直接退出。
五、函数值与闭包:函数是一等公民
Go 的函数是一等公民——可以当变量传、当参数传、当返回值:
// 函数类型:func(int) int
type op func(int) int
func apply(n int, f op) int {
return f(n)
}
func main() {
double := func(x int) int {
return x * 2
}
fmt.Println(apply(5, double)) // 10
}闭包:匿名函数可以"捕获"外层变量,形成闭包:
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
c1 := counter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c1()) // 3
c2 := counter()
fmt.Println(c2()) // 1 // 独立的 n,互不干扰
}每次调用 counter() 都会创建一个新的 n,返回的闭包绑定这个 n,闭包存活 n 就存活——这就是闭包的"捕获"语义。
闭包常见用途:
- 回调函数
- 工厂模式
- 延迟执行
六、defer 再回顾:和函数返回的配合
defer 在函数返回前执行,所以它能看到命名返回值,可以修改返回值:
func increment(n int) (res int) {
defer func() {
res++ // defer 里改 res,会影响最终返回值
}()
return n // 先给 res 赋值 n,然后 defer 执行 res++
}
increment(5) // 返回 6这个技巧偶尔用来包装返回值、统一处理错误,新手知道这个现象就行。
七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 错误不检查 | v, _ := fn() 把 error 扔了,错了不知道 | 只要返回 error,必须检查 |
| 长函数用裸返回 | 命名返回值 + 裸返回,谁也不知道返回什么 | 长函数显式写 return res, err |
| 业务错误用 panic | 正常业务错误抛 panic,调用者不得不 recover | 业务错误永远返回 error |
| 闭包捕获循环变量 | for 里开 goroutine,所有闭包都抓同一个变量 | 循环里 v := v 再传进去 |
最后那个"闭包捕获循环变量"是 Go 新手并发编程第一坑,我们讲到 goroutine 时再细说。
相关与延伸
下一篇:数组、切片与 map——Go 的核心数据结构;函数参数传递:Go 也是值传递,和 C 一样,传指针才会修改外面——对应 C 值传递;Go 的 ABI 和调用约定见进阶篇;闭包捕获变量涉及栈上分配 vs 堆逃逸,详见 逃逸分析。
一句话总结
Go 函数:多返回值是核心特色,(result, error) 是 Go 风格的标准模式;error 是值不是异常,if err != nil 尽早返回;命名返回值+裸返回短函数可用、长函数慎用;函数是一等公民,闭包捕获外层变量、要小心循环变量陷阱。