学习一门编程语言我通常会从下面这些内容入手,本次学习 Golang 语法也是。在此形成一份语法摘要。
- 基础:变量、数据类型、运算符、注释
- 控制:条件、循环、分支
- 数据结构:数组、列表、元组、集合、字典
- 函数:定义、参数、返回值、嵌套
- 面向对象:类、对象、继承、多态、封装
- 其他:异常、模块化、新特性
注释
单行注释 //
// 这是一个行注释
x := 42 // 行尾注释
多行注释
/*
这是一个多行注释
可以跨越多行
*/
官方强烈推荐使用 // 行注释,即使是多行也用多个 //:
打印(Print)
在 Go 语言中,没有内置的 print 关键字,但标准库提供了多个用于输出的函数。
◾最常用:fmt 包(格式化输出)
-
fmt.Println。输出到os.Stdout,带换行的打印。自动在参数间加空格,末尾加换行符\n。支持任意类型(调用其String()方法或默认格式)fmt.Println("Hello", "World") // 输出: Hello World\n fmt.Println(42, true, []int{1,2,3}) // 输出: 42 true [1 2 3]\n -
fmt.Print。输出到os.Stdout,不带换行,参数间不自动加空格,需手动控制格式。fmt.Print("Hello") fmt.Print("World") // 输出: HelloWorld(无空格、无换行) -
fmt.Printf。输出到os.Stdout,格式化输出(类似 C 的printf)name := "Alice" age := 30 fmt.Printf("Name: %s, Age: %d\n", name, age) // 输出: Name: Alice, Age: 30常用格式动词:
动词 说明 示例 %v默认格式 fmt.Printf("%v", x)%+v结构体显示字段名 fmt.Printf("%+v", user)%#vGo 语法表示(可复制回代码) fmt.Printf("%#v", []int{1,2})→[]int{1, 2}%T类型 fmt.Printf("%T", x)→int%s字符串 "hello"%d十进制整数 42%x十六进制 2a%f浮点数 3.141592%t布尔值 true推荐:优先用
%v,结构体用%+v,调试用%#v
◾其他打印方式
-
println和print(内置函数,输出到stderr,仅用于调试!官方文档明确要求 “不要在生产代码中使用!”)print("hello") // 输出到 stderr,无空格、无换行 println("world") // 输出 + 换行 -
log包(带时间戳的日志输出),输出到stderr,适合写日志,不适合普通打印import "log" log.Println("This is a log message") log.Printf("Error: %v", err) -
直接写
os.Stdout/os.Stderr。底层操作,一般不需要,fmt内部就是基于这些实现的。import "os" os.Stdout.Write([]byte("hello\n"))
记住:在 Go 中,fmt 是你打印输出的唯一朋友。
变量(Variables)
变量声明与使用可以分成两行来写。
var a int
a = 10
因为 Golang 是支持类型推断的,我一般习惯在同一行完成声明和初始化。甚至如果是在函数内部进行变量声明,连 var关键字都可以省去,但要使用海豹运算符。
var b string = "Hello" // 完整的声明 + 初始化
var c = 3.14 // 借助类型推断,可以这样简写
func main() {
d := "World" // 简短变量声明,只能在函数内部使用
}
多变量声明
var e, f int = 5, 10 // 多个变量的完整声明
g, h := 15, "Go语言" // 多个变量的简短声明
var ( // 多变量声明分组
m = 100
n = "分组声明"
o = false
)
当一个变量被声明但未显式初始化时,Go 会自动将其赋值为该类型的零值。不同数据类型的零值 (Zero Value) 是不一样的。
// 零值(Zero Value)
var i int // 0
var j float64 // 0.0
var k bool // false
var l string // ""
Golang 在变量的使用上还是比较常规的。
常量(Constants)
// 全局常量声明
const Pi = 3.1415926
const Version = "1.0.0"
// 常量分组声明
const (
StatusOK = 200
StatusError = 500
StatusNotFound = 404
)
常量计数器 (iota)
// 常量计数器(iota)
const (
Monday = iota // iota 从0开始,每行递增1
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
)
// 常量表达式
const (
// 位运算示例
Bit0 = 1 << iota // 1 << 0 = 1
Bit1 // 1 << 1 = 2
Bit2 // 1 << 2 = 4
Bit3 // 1 << 3 = 8
)
类型化常量
const MaxAge int = 120
无类型常量(可以隐式转换)
const Count = 100
var num1 int = Count
var num2 float64 = Count
常量不能使用简短变量声明语法 :=
数据类型(Data Types)
Go 语言(Golang)的数据类型体系清晰、简洁,分为 基本数据类型 和 复合/引用数据结构。以下是全面整理:
| 分类 | 类型 |
|---|---|
| 基本类型 | bool, int/uint, float, complex, string, byte, rune |
| 复合类型 | array, slice, map, struct |
| 引用类型 | slice, map, channel, pointer, function(底层是指针) |
| 其他 | interface{}, error, chan |
其中日常开发中最常用的是基本类型 (int, string, bool, float64) 和数据结构 (slice, map, struct)。
可以使用 fmt.Printf("%T", x) 来查看变量 x 的类型(Type)。
◾布尔类型 (bool)
取值为 true 或 false
var ok bool = true
◾数值类型
-
整数类型 (int),有符号
int是最常用的整数类型,除非有特殊需求(如节省内存、协议对齐),否则优先用int。类型 范围 大小 int8-128 ~ 127 1 字节 int16-32768 ~ 32767 2 字节 int32-2³¹ ~ 2³¹-1 4 字节 int64-2⁶³ ~ 2⁶³-1 8 字节 int平台相关(32/64位系统) 通常 8 字节(64位机) -
无符号整数 (uint)
byte是uint8的别名,rune是int32的别名(用于 Unicode 字符)。类型 范围 大小 uint80 ~ 255 1 字节(等价于 byte)uint160 ~ 65535 2 字节 uint320 ~ 2³²-1 4 字节 uint640 ~ 2⁶⁴-1 8 字节 uint平台相关 同 intuintptr存储指针地址 平台相关 -
浮点数 (float)
var pi float64 = 3.1415926类型 精度 大小 float32约 7 位十进制 4 字节 float64约 15 位十进制 8 字节(默认) -
复数(较少用)
complex64(实部/虚部为float32);complex128(实部/虚部为float64)。c := 3 + 4i // 默认 complex128
◾字符串类型 (string)
字符串类型是 UTF-8 编码的只读字节序列。在 Golang 中,字符串是不可变的,修改需创建新字符串。
s := "Hello, 世界"
如果要表示其他语言中的 char 类型,可以使用内置类型 rune,rune 实际上是 int32 的别名,并且 rune 基于 Unicode 而非单字节。
var r rune = 'A' // ASCII 字符
var r2 rune = '中' // 中文字符
var r3 rune = '😊' // Emoji
🔔 注意:字符字面量用 单引号 ',字符串用双引号 "。
运算符
Golang 的运算符设计简洁、明确,不支持运算符重载。
| 类别 | 运算符 | 名称 / 含义 | 说明 | 示例 |
|---|---|---|---|---|
| 算术运算符 | + |
加法 | 数值相加或字符串拼接 | 3 + 2 → 5;"a" + "b" → "ab" |
- |
减法 | 数值相减 | 5 - 2 → 3 |
|
* |
乘法 | 数值相乘 | 4 * 3 → 12 |
|
/ |
除法 | 整数除法截断(向零取整),浮点正常除 | 7 / 2 → 3;7.0 / 2 → 3.5 |
|
% |
取模(余数) | 仅用于整数类型 | 7 % 3 → 1 |
|
++ |
自增 | 只能作为独立语句,不能用于表达式 | i++ ✅;a = i++ ❌ |
|
-- |
自减 | 同上 | i-- ✅ |
|
| 比较运算符 | == |
等于 | 判断两个值是否相等(类型必须可比较) | x == y |
!= |
不等于 | x != y |
||
< |
小于 | 支持数字、字符串(字典序) | "a" < "b" → true |
|
<= |
小于等于 | |||
> |
大于 | |||
>= |
大于等于 | |||
| 逻辑运算符 | && |
逻辑与 | 短路求值:左为 false 时右不执行 |
a > 0 && a < 10 |
| ` | ` | 逻辑或 | ||
! |
逻辑非 | 取反布尔值 | !ok |
|
| 位运算符 (仅整数) | & |
按位与 | 二进制位逐位与 | 5 & 3 → 1(101 & 011 = 001) |
| ` | ` | 按位或 | ||
^ |
按位异或 | 相同为 0,不同为 1 | 5 ^ 3 → 6 |
|
&^ |
位清除(AND NOT) | 清除左操作数中右操作数为 1 的位 | 0b1111 &^ 0b1010 → 0b0101 |
|
<< |
左移 | 左操作数左移右操作数位(高位丢弃) | 1 << 3 → 8 |
|
>> |
右移 | 无符号右移(逻辑右移) | 8 >> 2 → 2 |
|
| 赋值运算符 | = |
赋值 | 基本赋值 | x = 10 |
+= |
加后赋值 | x += y 等价于 x = x + y |
||
-= |
减后赋值 | |||
*= |
乘后赋值 | |||
/= |
除后赋值 | |||
%= |
模后赋值 | |||
&=, ` |
=, ^=, &^=` |
位运算赋值 | ||
<<=, >>= |
移位赋值 | x <<= 2 |
||
| 指针/地址运算符 | & |
取地址 | 获取变量内存地址 | p := &x |
* |
解引用 | 通过指针访问值 | v := *p |
|
| 通道操作符 | <- |
发送 / 接收 | 用于 channel 通信 | ch <- data(发送) data := <-ch(接收) |
| 其他 | _ |
空标识符 | 用于丢弃不需要的值(非运算符,但常配合使用) | _, err := fmt.Println("hi") |
◾重要限制与注意事项:
- 不支持三元运算符,必须用
if-else实现条件赋值 - 不支持运算符重载,所有运算符行为固定,不可自定义
++和--是语句,不是表达式,不能写成a = i++- slice、map、function 不可比较,不能用
==,但可以和nil比较 - 字符串支持
+拼接,但不支持-等其他算术运算,"a" + "b"✅;"ab" - "b"❌ - 位运算仅适用于整数类型。浮点数、字符串等不能进行位运算
- 通道
<-方向由上下文决定,在赋值左边是接收,在右边是发送
◾运算符优先级 (从高到低,官方简化版):
( ) // 圆括号(最高)
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
|| // 最低
建议多用括号 () 明确意图,避免依赖优先级。
条件语句(Conditionals)
Golang 在语法上比较精炼,因此仅有 2 种条件语句:if-else、switch-case
if-else
土土的基本用法:
num := 10
if num > 0 {
fmt.Println("num是正数")
}
连续使用:
score := 85
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
if 语句中的初始化语句:
if age := 18; age >= 18 {
fmt.Println("已成年")
} else {
fmt.Println("未成年")
}
注意,age 变量的作用域仅限于 if-else 语句块。
switch-case
土土的基本用法
day := 3
switch day {
case 1:
fmt.Println("星期一")
case 2:
fmt.Println("星期二")
case 3:
fmt.Println("星期三")
case 4:
fmt.Println("星期四")
case 5:
fmt.Println("星期五")
case 6, 7:
fmt.Println("周末")
default:
fmt.Println("无效的星期")
}
switch 语句中的初始化语句
switch temperature := 25; {
case temperature < 0:
fmt.Println("寒冷")
case temperature >= 0 && temperature < 20:
fmt.Println("凉爽")
case temperature >= 20 && temperature < 30:
fmt.Println("舒适")
default:
fmt.Println("炎热")
}
switch 语句中的 fallthrough(默认不穿透)
grade := "A"
switch grade {
case "A":
fmt.Println("优秀")
fallthrough // 继续执行下一个case
case "B":
fmt.Println("良好")
case "C":
fmt.Println("及格")
default:
fmt.Println("不及格")
}
类型判断 (type switch)
var x interface{}
x = 100
switch v := x.(type) {
case int:
fmt.Printf("x是int类型,值为%d\n", v)
case string:
fmt.Printf("x是string类型,值为%s\n", v)
case bool:
fmt.Printf("x是bool类型,值为%v\n", v)
default:
fmt.Printf("x是未知类型,值为%v\n", v)
}
语法上依旧平平无奇。
循环(Loop)
Golang 在循环上就更吝啬了,仅仅只有 for-Loop 这一种循环语句。一个标准的 for 循环语法如下(类似C语言):
for i := 0; i < 5; i++ {
fmt.Printf("i = %d\n", i)
}
其中 i: = 0 叫做初始化,i < 5 叫做循环条件,i++ 叫做更新。这三个部分都是可以省略的。
// for循环(省略初始化)
j := 0
for ; j < 5; j++ {
fmt.Printf("j = %d\n", j)
}
// for循环(省略初始化和更新)
k := 0
for k < 5 {
fmt.Printf("k = %d\n", k)
k++
}
// 无限循环(Go语言没有while循环)
count := 0
for {
fmt.Printf("循环计数: %d\n", count)
count++
if count >= 3 {
break // 跳出循环
}
}
for 循环遍历数组
numbers := [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(numbers); i++ {
fmt.Printf("numbers[%d] = %d\n", i, numbers[i])
}
for range 循环(遍历数组、切片、映射等)
// 普普通通的写法,numbers := [5]int{1, 2, 3, 4, 5}
for index, value := range numbers {
fmt.Printf("numbers[%d] = %d\n", index, value)
}
// 如果不需要用到索引(index),能用 '_' 来省略索引
for _, value := range numbers {
fmt.Printf("值 = %d\n", value)
}
// 反过来,如果只关心索引:
for index := range numbers {
fmt.Printf("索引 = %d\n", index)
}
当然 continue 也是可以用的。
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // 跳过偶数
}
fmt.Printf("奇数: %d\n", i)
}
嵌套循环
for i := 1; i <= 9; i++ {
for j := 1; j <= i; j++ {
fmt.Printf("%d*%d=%d\t", j, i, i*j)
}
fmt.Println()
}
总之,Golang 在 for 循环上的语法和 C 差不太多。
复合/引用数据结构(Composite Types)
这些不是 “基本类型”,但属于 Go 内置的核心数据结构。
-
数组 (Array):长度固定(长度是类型的一部分)。属于值类型(赋值会拷贝整个数组)。数组很少直接使用,通常用 slice 替代
var arr [3]int = [3]int{1, 2, 3} // 或 arr2 := [3]string{"a", "b", "c"} -
切片 (Slice):最常用的数据类型。动态长度,底层基于数组。属于引用类型(传递的是 header,包含指针、长度、容量)。
append是内置函数,用于向 slice 添加元素s := []int{1, 2, 3} s = append(s, 4) // 动态扩容 -
映射 (Map):当字典用。是一种键值对集合,类似
Python | dict、Java | HashMap。属于引用类型,必须用make初始化(或字面量),并且映射 (Map) 的 key 必须是可比较类型(不能是 slice/map/function)m := make(map[string]int) m["age"] = 25 -
结构体 (Struct):自定义复合类型,组合多个字段
type Person struct { Name string Age int } p := Person{Name: "Alice", Age: 30} -
指针 (Pointer):存储变量的内存地址。通过
&取地址,*解引用。由于 Golang 没有指针运算(不能p++),更安全。x := 42 p := &x // p 是 *int *p = 100 // 修改 x 的值 -
函数类型 (Function):在 Golang 中,函数是一种数据类型,可作为变量、参数、返回值
-
通道 (Channel):并发相关。用于 goroutine 之间通信
ch := make(chan int) go func() { ch <- 42 }() value := <-ch -
错误类型 (error):
error是内置接口,用于错误处理 -
其他类型:空接口类型
interface{}可表示任意类型,在 Go 1.18+ 之后推荐用any,实质上二者是等价的
指针、切片、map、channel、函数、接口这些引用类型的零值为 nil。nil 源于Lisp 语言,nil 不是关键字,而是一个无类型的零值常量。
函数(Function)
函数定义的基本语法格式:
func funcName(varName1 varType1, varName2 varType2) returnType {
Statement1;
Statement2;
...
return Expression
}
例如:
func add(a int, b int) int {
return a + b
}
相同类型的多个参数,参数类型声明可以简化。例如上面例子中的 (a int, b int) 可以简化为 (a, b int)。
在 Go 中还有一点需要注意的是,所有参数传递和赋值都是按值复制的 (pass by value)。
变参函数(Variadic Function)
参数部分 numbers ...int 表示这是一个可变参数函数,意思是你可以传入任意数量的 int 类型参数(包括0个)
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
最关键的一句是 for _, num := range numbers。首先使用 for range 循环遍历 numbers(它在函数内部其实是一个 []int 切片)。_ 是 Go 中的空白标识符,表示我们忽略索引(因为这里不需要知道第几个元素)。num 是当前遍历到的数值。如果你学习过 Python,这句话表达的意思是 for num in numbers(for Python)。
返回值(Return)
在之前的函数定义部分,其中第三个 int 表示返回值为 int 类型,且此时函数只有一个返回值。
vvv
func add(a int, b int) int {
return a + b
}
在函数有多个返回值的情况下,返回值的类型声明要写成如下的格式:
vvvvvvvvvv
func swap(a, b int) (int, int) {
return b, a
}
另外,有时候在返回值声明内会指定返回值的名称,这个部分表示命名返回值(可以在函数内部直接使用):
func divide(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return // 裸返回(bare return),自动返回所有命名返回值
}
fmt.println(divide(3, 1)) // 3 0
// 等价于
func divide2(a, b int) (int, int) {
var quotient int = a / b
var remainder int = a % b
return quotient, remainder
}
但很显然,上面的写法更加浓缩,在首行暴露的信息也更精炼。更推荐 func divide(a, b int) (quotient, remainder int) 这种写法。
函数值也是一种数据类型
在先前的小节提到过,函数值也是一种数据类型。因此其他数据类型能做的事情,函数值作为一种数据类型也能做。
函数递归
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
函数作为参数
func apply(op func(int, int) int, a, b int) int {
return op(a, b)
}
函数作为返回值
func makeAdder(x int) func(int) int {
// 返回一个闭包函数
return func(y int) int {
return x + y
}
}
函数是一等公民(first-class citizen),函数值(function value)是一种数据类型,可以像其他值一样被赋值、传递、返回。(First-class citizen,编程术语,用来描述某种语言实体(如函数、类型、变量等)是否被语言本身充分支持,并能像其他基本值一样自由使用)
面向对象(OOP)
需要注意的是,不像 Java/Python 的一切皆对象,Golang 在设计上并没有 “对象” 这个概念,Go 是基于类型和值的静态语言。但这并不代表基于 Golang 的语法不能实现 OOP。
Go 的哲学是组合优于继承 (Composition over Inheritance)。通过 结构体 (struct)、方法 (method)、接口 (interface),以及组合 (composition) 可以实现 OOP 的核心思想:封装、多态、组合复用。并且 Golang 在设计上刻意避免了继承。
OOP 三大特性在 Go 中的实现:
- 封装(Encapsulation):通过 struct 字段首字母大小写控制可见性(大写导出,小写私有)
- 多态(Polymorphism):通过 interface 实现
- 复用(代码重用):通过 嵌入(embedding) 实现组合(不是继承!)
封装(Struct + 方法)
定义类型(类似“类”)
// 文件:person.go
package main
import "fmt"
// Person 类似一个“类”
type Person struct {
name string // 小写:包内私有
Age int // 大写:公开
}
// 方法(绑定到 Person 类型)
func (p Person) SayHello() {
fmt.Printf("Hello, I'm %s, %d years old\n", p.name, p.Age)
}
// 提供“构造函数”(Go 没有内置构造函数,但可自定义)
func NewPerson(name string, age int) *Person {
return &Person{name: name, Age: age}
}
实际调用过程:
//1 直接初始化 (不调用 NewPerson)
alice := Person{name: "Alice", Age: 25} // 实例化一个 Person
alice.SayHello() // 调用 SayHello 方法
//2 通过“构造函数”初始化(显式调用)
p2 := NewPerson("Bob", 25) // 函数内部创建并返回一个 *Person 指针;
其中 p 是方法的接收者,是一个实例变量名。这部分也是方法 (Method) 与函数 (Function) 在语法形式上不同的地方,并且一个方法只能有一个接收者。
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
func (receiverVarName ReceiverType) MethodName() {
functionBody
}
这里用的是 值接收者:(p Person)。如果改成指针接收者:(p *Person),那么 p 就是指向原对象的指针,可以修改原始数据。在 Go 中,所有参数传递和赋值都是按值复制的 (pass by value)。两者的区别是:如果你传一个结构体,Go 会复制整个结构体;而如果你传一个指针,Go 会复制这个指针的值(地址),但指向的是同一个对象。这里的详细区别这里就不展开讲了。
TIPS:name 是私有的,外部不能直接访问,只能通过方法操作(实现封装)。
多态(Interface)
定义接口
// 定义一个接口
type Greeter interface {
SayHello()
}
多个类型实现同一接口
type Dog struct{ Name string }
func (d Dog) SayHello() {
fmt.Printf("Woof! I'm %s\n", d.Name)
}
type Cat struct{ Name string }
func (c Cat) SayHello() {
fmt.Printf("Meow! I'm %s\n", c.Name)
}
多态调用
func Greet(g Greeter) {
g.SayHello() // 运行时决定调用哪个实现
}
func main() {
p := NewPerson("Alice", 30)
d := Dog{"Buddy"}
c := Cat{"Whiskers"}
Greet(p) // Hello, I'm Alice...
Greet(d) // Woof! I'm Buddy
Greet(c) // Meow! I'm Whiskers
}
这就是多态,同一个接口,不同行为。
代码复用(组合,Embedding)
◾Go 没有继承,但可以用 嵌入 (embedding) 实现 “字段和方法的提升”
// 示例:Employee “包含” Person
type Employee struct {
Person // 匿名嵌入(不是继承!)
Company string
}
// Employee 自动获得 Person 的所有字段和方法
e := Employee{
Person: Person{name: "Bob", Age: 28},
Company: "Google",
}
e.SayHello() // 调用 Person 的方法
fmt.Println(e.Age) // 直接访问提升的字段
值得注意的是,Employee 不是 Person 的子类,Employee 和 Person 是两个独立的类型。嵌入本质上只是语法糖,是编译器自动 “提升” 字段和方法。
◾Go 没有方法覆盖(Override),但你可以重新定义同名方法
func (e Employee) SayHello() {
fmt.Printf("Hi, I'm %s from %s\n", e.name, e.Company)
}
现在 e.SayHello() 调用的是 Employee 的版本,不是 Person 的。但这不是覆盖,而是新定义。如果想调用原始方法,需显式指定:
e.Person.SayHello() // 调用嵌入类型的原始方法
Go 的 interface 是隐式实现的 (Duck Typing,“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。”)。你不需要显式声明 type Dog implements Greeter,只要 Dog 有 SayHello() 方法,它就自动满足 Greeter 接口。这种解耦的方式灵活且易于 mock 测试。
总之,Go 不支持传统 OOP,但通过 struct + method + interface + embedding 进行更简洁、更灵活的实现。 面向组合编程,它没有 class 的面向对象,而是实现了一种面向接口和组合的编程。
异常(Error)
Go 语言没有 try-catch-finally 异常机制,而是采用 显式错误处理 的哲学。错误(error)在 Go 中是一个内置接口类型,任何实现了其 Error() string 方法的类型都可以作为错误值。
// error 接口的定义
type error interface {
Error() string
}
这种设计强制开发者在编码时就考虑并处理错误路径,使程序的健壮性和可读性更强。
错误处理基本模式
函数通常将 error 作为最后一个返回值。调用者必须显式检查该值是否为 nil(nil 表示无错误)。
// 标准库 os.Open 的签名
func Open(name string) (*File, error)
// 调用与处理
file, err := os.Open("config.txt")
if err != nil {
// 处理错误:记录日志、返回、退出等
log.Fatal(err)
}
// 只有 err == nil 时,file 才是有效的
defer file.Close()
永远不要忽略错误!即使你认为“这里不可能出错”,也至少应该记录它。
创建错误
Go 提供了多种创建错误的方式:
errors.New: 创建一个简单的、仅包含静态字符串的错误。
import "errors"
func checkAge(age int) error {
if age < 0 {
return errors.New("年龄不能为负数")
}
return nil
}
fmt.Errorf: 创建格式化的错误信息,非常常用。
import "fmt"
func divide(a, b float64) (float64, error) {
if b == 0.0 {
return 0, fmt.Errorf("除零错误: 无法计算 %f / %f", a, b)
}
return a / b, nil
}
自定义错误类型: 当需要携带更多上下文信息(如错误码、字段名等)时,可以定义自己的结构体并实现 error 接口。
type ValidationError struct {
Field string
Message string
}
// 实现 error 接口
func (e ValidationError) Error() string {
return fmt.Sprintf("字段 '%s': %s", e.Field, e.Message)
}
func validateEmail(email string) error {
if !isValid(email) {
return ValidationError{
Field: "email",
Message: "邮箱格式不正确",
}
}
return nil
}
错误包装与检查(Go 1.13+)
为了在多层函数调用中保留原始错误信息,Go 支持错误包装 (Error Wrapping)。
包装错误: 使用 fmt.Errorf 并配合 %w 动词。
func processFile(path string) error {
data, err := ioutil.ReadFile(path)
if err != nil {
// 包装底层错误,添加上下文
return fmt.Errorf("读取文件 %s 失败: %w", path, err)
}
// ... 处理 data
return nil
}
检查错误链:
errors.Is(err, target): 检查错误链中是否包含特定的目标错误(即使被包装过)。
// 定义一个包级别的错误变量用于比较
var ErrNotFound = errors.New("未找到")
if errors.Is(err, ErrNotFound) {
// 处理“未找到”的情况
}
errors.As(err, &target): 将错误链中的某个具体类型错误提取出来。
var validationErr ValidationError
if errors.As(err, &validationErr) {
// 现在可以访问 validationErr.Field 和 .Message
fmt.Printf("验证失败字段: %s\n", validationErr.Field)
}
关键要点总结
- 显式处理: 错误是值,必须被检查。
nil即成功:error返回值为nil表示操作成功。- 包装优于替换: 在向上层返回错误时,优先使用
fmt.Errorf+%w进行包装,以保留完整的错误上下文。 - 使用
errors.Is和errors.As: 这是现代 Go 中检查和转换错误的标准方式,比类型断言更安全、更灵活。 - 提供有意义的信息: 错误信息应清晰、简洁,并包含足够的上下文(如文件名、参数值等)以便于调试。
通过这套简单而强大的机制,Go 鼓励开发者编写出对错误路径有充分考量的、可靠的代码。